diff --git a/.fvmrc b/.fvmrc index 391983cd..827b5564 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.41.2" + "flutter": "3.47.5" } diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 26be83ff..1e0e6eaa 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -6,8 +6,9 @@ ## Repository layout - `packages/ack`: core runtime validation library. -- `packages/ack_annotations`: annotations for schema-first `@AckInfer()`, - class-first `@AckModel()`, and frozen legacy `@AckType()` generation. +- `packages/ack`: runtime validation and schema-generation annotations. +- `packages/ack_annotations`: compatibility re-exports for schema-first + `@AckInfer()`, class-first `@AckModel()`, and frozen legacy `@AckType()`. - `packages/ack_generator`: build_runner generator + unit/integration tests. - `packages/ack_firebase_ai`: Firebase AI schema adapter. - `packages/ack_json_schema_builder`: JSON Schema adapter. diff --git a/.github/flutter-releases.json b/.github/flutter-releases.json index 78083a78..fbd50fba 100644 --- a/.github/flutter-releases.json +++ b/.github/flutter-releases.json @@ -1,6 +1,10 @@ { "description": "Flutter releases that this repository's workflows may install. Each entry pins the SHA-256 checksum of the official Linux archive and the Dart SDK that ships inside it. Copy both values from https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json before adding a version.", "releases": { + "3.47.5": { + "dart": "3.13.4", + "sha256": "2132e990f236f8d22e7c6314b29a191a95b10d7cbcfec9b4e2e303d996652cbb" + }, "3.35.0": { "dart": "3.9.0", "sha256": "ff2d9474d768fdb61c1b8d6b76fd4eb98235c271e68b10abf41bd9fb41bc273d" diff --git a/.github/workflows/preflight.yml b/.github/workflows/preflight.yml index a96f6beb..92e305d8 100644 --- a/.github/workflows/preflight.yml +++ b/.github/workflows/preflight.yml @@ -45,12 +45,13 @@ jobs: - name: Resolve workspace dependencies run: dart pub get - # The complete workspace cannot resolve on the minimum SDK, because - # flutter_test pins an older test_api than ack_generator's analyzer floor - # allows. The pure-Dart members still must resolve, so they get their own - # staged workspace. + # Runtime packages keep their Dart 3.9 minimum. The generator and its + # example require Dart 3.13 and use a separate staged workspace. - name: Stage the pure-Dart workspace - run: dart scripts/stage_min_sdk_workspace.dart "${{ runner.temp }}/min-dart" + run: dart scripts/stage_min_sdk_workspace.dart "${{ runner.temp }}/min-dart" --runtime-only + + - name: Stage the generator workspace + run: dart scripts/stage_min_sdk_workspace.dart "${{ runner.temp }}/min-generator" - name: Set up the minimum Dart SDK uses: dart-lang/setup-dart@6afc89df92d6eb3834022f73cd65adc8cdfcb92d # v1 @@ -90,6 +91,18 @@ jobs: echo "::endgroup::" done + - name: Set up the minimum generator SDK + uses: dart-lang/setup-dart@6afc89df92d6eb3834022f73cd65adc8cdfcb92d # v1 + with: + sdk: '3.13.0' + + - name: Check the generator on its minimum SDK + working-directory: ${{ runner.temp }}/min-generator/packages/ack_generator + run: | + dart pub get + dart analyze --fatal-infos + dart test + minimum-flutter-sdk: name: Minimum Flutter SDK runs-on: ubuntu-24.04 diff --git a/README.md b/README.md index 0bc1bd2e..0a942014 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,15 @@ For AI agents: start at [`/llms.txt`](https://concepta.dev/documentation/ack/ref - **Validate external payloads**: Guard API and user inputs by validating required fields, types, and constraints at boundaries - **Single source of truth**: Define data structures and rules in one place - **Less boilerplate**: Minimize repetitive validation and JSON conversion code -- **Type safety**: Generate immutable models for hand-written Ack schemas with `@AckInfer()` -- **Class-first generation**: Derive validated codec schemas from hand-written Dart classes with `@AckModel()` +- **Type safety**: Generate immutable models from Ack schemas with `@Schemable()` +- **Class-first generation**: Derive validated codec schemas from primary-constructor classes with `@Schemable()` ## Packages This repository is a monorepo containing: - **[ack](./packages/ack)**: Core validation library with a fluent schema-building API, codecs, and JSON Schema export -- **[ack_annotations](./packages/ack_annotations)**: The `@AckInfer()`, `@AckModel()`, and deprecated legacy `@AckType()` annotations +- **[ack_annotations](./packages/ack_annotations)**: Compatibility re-exports for annotations now owned by `ack` - **[ack_generator](./packages/ack_generator)**: Generates models from schemas and schemas from hand-written models - **[ack_firebase_ai](./packages/ack_firebase_ai)**: Firebase AI (Gemini) schema converter for structured-output generation - **[ack_json_schema_builder](./packages/ack_json_schema_builder)**: Converter to `json_schema_builder` schemas @@ -122,18 +122,17 @@ if (result.isOk) { ## Code generation -Generate immutable models for hand-written schemas with `@AckInfer()`. Add -`ack_annotations` to `dependencies` and `ack_generator` + `build_runner` to +Generate immutable models for hand-written schemas with `@Schemable()`. Add +`ack` to `dependencies` and `ack_generator` + `build_runner` to `dev_dependencies`, then annotate a top-level schema: ```dart import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'user.ack.dart'; part 'user.ack.g.dart'; -@AckInfer() +@Schemable() final userSchema = Ack.object({ 'name': Ack.string().minLength(2), 'email': Ack.string().email(), @@ -155,7 +154,7 @@ print(user.name); // String print(user.toJson()); // {'name': 'Alice', 'email': 'alice@example.com'} ``` -`@AckInfer()` supports objects, primitives, lists, enums, bidirectional codecs, +`@Schemable()` supports objects, primitives, lists, enums, bidirectional codecs, named recursion, and discriminated unions. One-way transforms are rejected because a generated model must be encodable. See the [Model Code Generation guide](docs/core-concepts/typesafe-schemas.mdx). @@ -168,7 +167,6 @@ removed in Ack 2.0: ```dart import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'legacy_user.g.dart'; @@ -177,12 +175,11 @@ final userSchema = Ack.object({'name': Ack.string()}); ``` This still generates `UserType`, including its Map interface, typed getters, -`parse` / `safeParse`, and `.args`. New code should use `@AckInfer()` or -`@AckModel()`. +`parse` / `safeParse`, and `.args`. New code should use `@Schemable()`. | Ack 1.1 source | Optional immutable-model migration | |---|---| -| Keep `@AckType()` | Rename it to `@AckInfer()` | +| Keep `@AckType()` | Rename it to `@Schemable()` | | Keep `part 'file.g.dart';` | Add `file.ack.dart` and `file.ack.g.dart` parts | | Use `*Type`, Map access, and `.args` | Use the generated class, typed fields, `parse`, `fromJson`, and `toJson` | @@ -190,19 +187,15 @@ Legacy and modern declarations may coexist when they are unrelated. A nested reference graph cannot cross between them; migrate that connected graph together. -Already own the model class? Use `@AckModel()` to derive a codec schema from +Already own the model class? Use `@Schemable()` to derive a codec schema from constructor-backed fields while keeping the class hand-written. A class named `Account` receives an `AccountSchema` facade for parsing, encoding, schema export, and nested composition; the backing codec remains private: ```dart -@AckModel() -final class Account with _$AccountAck { - const Account({required this.name}); - - @MinLength(2) - final String name; - +@Schemable() +final class Account({@MinLength(2) required final String name}) + with _$AccountAck { static final fromJson = AccountSchema.fromJson; } ``` diff --git a/docs/api-reference/index.mdx b/docs/api-reference/index.mdx index d3536773..c18a7bc0 100644 --- a/docs/api-reference/index.mdx +++ b/docs/api-reference/index.mdx @@ -341,7 +341,7 @@ For `Ack.discriminated(...)` constraints with `@AckInfer`, see **Example:** ```dart import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; +import 'package:ack/annotations.dart'; part 'user.ack.dart'; part 'user.ack.g.dart'; diff --git a/docs/core-concepts/typesafe-schemas.mdx b/docs/core-concepts/typesafe-schemas.mdx index a68cfa72..84aeae09 100644 --- a/docs/core-concepts/typesafe-schemas.mdx +++ b/docs/core-concepts/typesafe-schemas.mdx @@ -7,17 +7,18 @@ and generate a Dart model, or start with a Dart model and generate its schema. | Starting point | Annotation | Generated result | | --- | --- | --- | -| An Ack schema | `@AckInfer()` | An immutable Dart model | -| A Dart class | `@AckModel()` | An Ack codec schema and JSON helpers | +| An Ack schema | `@Schemable()` | An immutable Dart model | +| A Dart class | `@Schemable()` | An Ack codec schema and JSON helpers | There is no `@AckSchema()` annotation. `AckSchema` is the runtime type returned by factories such as `Ack.string()` and `Ack.object()`. -The two code-generation annotations are `@AckInfer()` and `@AckModel()`. +The canonical code-generation annotation is `@Schemable()`. The older +`AckInfer` and `AckModel` annotations remain deprecated spellings. ## Install the generator ```bash -dart pub add ack ack_annotations +dart pub add ack dart pub add --dev ack_generator build_runner ``` @@ -41,39 +42,26 @@ the same builders and generated parts. ```dart title="lib/models.dart" import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'models.ack.dart'; part 'models.ack.g.dart'; // Schema-first: write the schema; Ack generates Order. -@AckInfer() +@Schemable() final orderSchema = Ack.object({ 'id': Ack.string(), 'total': Ack.double().positive(), }); // Class-first: write Account; Ack generates AccountSchema. -@AckModel(caseStyle: AckCaseStyle.snake) -final class Account with _$AccountAck { - const Account({ - required this.displayName, - required this.email, - required this.middleName, - this.website, - this.role = 'member', - }); - - @MinLength(2) - final String displayName; - - @Email() - final String email; - - final String? middleName; - final Uri? website; - final String role; - +@Schemable(caseStyle: AckCaseStyle.snake) +final class Account({ + @MinLength(2) required final String displayName, + @Email() required final String email, + required final String? middleName, + final Uri? website, + final String role = 'member', +}) with _$AccountAck { static final fromJson = AccountSchema.fromJson; } ``` @@ -95,16 +83,16 @@ print(account.toJson()); // validated snake_case JSON The schema-first and class-first declarations may live in the same library. Ordinary `@JsonSerializable()` classes keep their separate `.g.dart` file, -but do not put `@AckModel()` and `@JsonSerializable()` on the same class. +but do not put `@Schemable()` and `@JsonSerializable()` on the same class. -## Schema-first with `@AckInfer()` +## Schema-first with `@Schemable()` Use schema-first generation when the wire contract is the primary artifact. The source schema owns validation, defaults, codecs, and JSON Schema export; Ack generates the stored Dart type around it. ```dart -@AckInfer() +@Schemable() final userSchema = Ack.object({ 'id': Ack.string().uuid(), 'email': Ack.string().email().nullable(), @@ -113,7 +101,7 @@ final userSchema = Ack.object({ ``` `userSchema` generates `User`: a trailing `Schema` is removed and no suffix is -added. Use `@AckInfer(name: 'AppUser')` when you need an exact class name. +added. Use `@Schemable(name: 'AppUser')` when you need an exact class name. Generated models provide: @@ -145,16 +133,16 @@ unresolved dynamic schema factories, and cross-library union branches. ### Schema-first unions -Each branch must be a named `@AckInfer()` object schema in the same library: +Each branch must be a named `@Schemable()` object schema in the same library: ```dart -@AckInfer() +@Schemable() final catSchema = Ack.object({'lives': Ack.integer()}); -@AckInfer() +@Schemable() final dogSchema = Ack.object({'breed': Ack.string()}); -@AckInfer() +@Schemable() final petSchema = Ack.discriminated( discriminatorKey: 'type', schemas: {'cat': catSchema, 'dog': dogSchema}, @@ -163,11 +151,13 @@ final petSchema = Ack.discriminated( This generates a sealed `Pet` base plus `Cat` and `Dog` branches. -## Class-first with `@AckModel()` +## Class-first with `@Schemable()` Use class-first generation when the Dart class is the primary artifact. Ack reads constructor-backed fields and generates a codec schema whose runtime -value is your class: +value is your class. Primary constructor fields are inferred automatically. +If a field type is another `@Schemable()` class, Ack composes its nested +schema without a nested-field annotation. ```dart final profile = ProfileSchema.parse(payload); // Profile @@ -187,7 +177,7 @@ entry point: - `ProfileSchema.schema` is the typed model codec used for composition; - `ProfileSchema.wireSchema` is the raw structural `Map` schema. -Every instantiable `@AckModel` class and implicit sealed-union branch must +Every instantiable `@Schemable` class and implicit sealed-union branch must be declared `final class`, contain only final stored fields, and apply its generated `_$ClassAck` mixin. The mixin supplies `toJson()`, `safeToJson()`, a typed `copyWith` where omission keeps the current value, explicit `null` @@ -224,7 +214,7 @@ static final Profile Function(Map) fromJson = ProfileSchema.fromJson; ``` -Use `@AckModel(description: ...)` and `@AckField(description: ...)` to export +Use `@Schemable(description: ...)` and `@AckField(description: ...)` to export schema descriptions. A single-line `@description` tag in a `///` or `/** */` doc comment is a fallback. Only the text after the tag, on the same line, is exported. Untagged prose does not become schema data. Explicit annotation text takes precedence. @@ -237,20 +227,17 @@ It does not change a nested model's own description. ```dart /// Tracks one unit of work. This prose is not exported. -@AckModel(description: 'A task the person can complete.') -final class Task with _$TaskAck { - const Task({required this.id, required this.title}); - - final String id; - +@Schemable(description: 'A task the person can complete.') +final class Task({ + required final String id, /// @description What to do. - final String title; -} + required final String title, +}) with _$TaskAck; TaskSchema.toJsonSchema()['description']; // 'A task the person can complete.' ``` -Use `@AckModel(schemaName: 'WireProfileSchema')` to override the exact public +Use `@Schemable(schemaName: 'WireProfileSchema')` to override the exact public facade name. It must be a public UpperCamel identifier. The private backing name remains derived from the model class, and no public lower-camel alias is generated. @@ -282,7 +269,7 @@ behavior. `@Optional()` is redundant when the constructor already treats the field as optional; `@NotNull()` is the annotation that changes null acceptance: ```dart -@AckModel() +@Schemable() final class Example with _$ExampleAck { const Example({this.label}); @@ -343,7 +330,7 @@ AckSchema colorSchema() => Ack.string().codec( encode: (color) => color.hex, ); -@AckModel() +@Schemable() final class Theme with _$ThemeAck { const Theme({required this.primary}); @@ -393,13 +380,13 @@ library are included automatically, and inherited constructor fields may use super parameters: ```dart -@AckModel(discriminatorKey: 'type') +@Schemable(discriminatorKey: 'type') sealed class Pet with _$PetAck { const Pet({required this.id}); final String id; } -@AckModel(discriminatorValue: 'cat') +@Schemable(discriminatorValue: 'cat') final class Cat extends Pet with _$CatAck { const Cat({required super.id, required this.lives}); final int lives; @@ -412,7 +399,7 @@ final class Dog extends Pet with _$DogAck { ``` The base and every concrete branch receive facades (`PetSchema`, `CatSchema`, -and `DogSchema`), including branches without their own `@AckModel` annotation. +and `DogSchema`), including branches without their own `@Schemable` annotation. Without an explicit `discriminatorValue`, the wire value is the verbatim class name (`Dog` above). Set explicit values when the wire format must remain stable @@ -423,10 +410,10 @@ use schema-first generation for those shapes. Raw object validation and class projection are separate decisions. `Ack.object(..., additionalProperties: true)` accepts unknown keys and preserves -them in the returned `JsonMap`. A later `@AckModel` projection then decides +them in the returned `JsonMap`. A later `@Schemable` projection then decides whether the Dart class rejects, discards, or captures those keys. -`@AckModel` uses `AckUnknownPropertyPolicy` through `unknownProperties`: +`@Schemable` uses `AckUnknownPropertyPolicy` through `unknownProperties`: - `reject` (default) fails validation on unknown properties; - `discard` accepts unknown properties but does not store them. Use it only for @@ -448,7 +435,7 @@ Nested class-first fields compose through the target facade automatically: ```dart import 'address.dart' as address; -@AckModel() +@Schemable() final class Order with _$OrderAck { const Order({required this.shipping}); final address.Address shipping; @@ -460,14 +447,14 @@ final class Order with _$OrderAck { Schema-first declarations can use the same facade explicitly: ```dart -@AckInfer() +@Schemable() final envelopeSchema = Ack.object({ 'address': address.AddressSchema.schema, 'history': Ack.list(address.AddressSchema.schema), }); ``` -Class-first fields may also use a model generated by `@AckInfer()`; Ack composes +Class-first fields may also use a model generated by `@Schemable()`; Ack composes through the generated `Address.$ack.schema`. Both directions work on the first clean build, including nullable values, lists, nested lists, and sets. @@ -490,8 +477,8 @@ mutually recursive contracts, use schema-first named `Ack.lazy` schemas. ## Which direction should you choose? -Choose `@AckInfer()` when you want to design the boundary schema first, generate -the whole model, or model a scalar or collection root. Choose `@AckModel()` +Choose `@Schemable()` when you want to design the boundary schema first, generate +the whole model, or model a scalar or collection root. Choose `@Schemable()` when you already own the class, want to keep methods and constructors in source, or prefer field annotations over a separate object schema. @@ -518,8 +505,9 @@ Both entries must cover the same annotated libraries. ## Build checklist -1. Add `ack` and `ack_annotations` to dependencies. +1. Add `ack` to dependencies. Existing applications may keep the + `ack_annotations` compatibility package. 2. Add `ack_generator` and `build_runner` to dev dependencies. 3. Declare both generated parts in each annotated library. -4. Choose `@AckInfer()` for schema-first or `@AckModel()` for class-first. +4. Choose `@Schemable()` for schema-first or `@Schemable()` for class-first. 5. Run `dart run build_runner build`. diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx index 2a8e90e9..104abcf6 100644 --- a/docs/getting-started/installation.mdx +++ b/docs/getting-started/installation.mdx @@ -26,10 +26,10 @@ dependencies: ## Optional code generator To generate models from Ack schemas or Ack schemas from hand-written models, -add the annotation and generator packages alongside `ack`: +add `ack` and the generator package: ```bash -dart pub add ack ack_annotations +dart pub add ack dart pub add --dev ack_generator build_runner ``` @@ -38,37 +38,31 @@ Or add them to your `pubspec.yaml`: ```yaml dependencies: ack: ^1.6.2 # Replace with latest version - ack_annotations: ^1.6.2 # Replace with latest version dev_dependencies: ack_generator: ^1.6.2 # Replace with latest version build_runner: ^2.4.0 ``` -The generator works in both directions. `@AckInfer()` marks a top-level schema -and generates an immutable model. `@AckModel()` marks a hand-written class and -generates a public `Schema` facade, a private Ack codec, and JSON -helpers. There is no `@AckSchema()` annotation; `AckSchema` is the runtime -schema type. +The generator works in both directions. `@Schemable()` marks a top-level +schema or a hand-written class. It generates an immutable model, or a public +`Schema` facade with a private Ack codec and JSON helpers. +`@AckInfer()` and `@AckModel()` remain deprecated spellings. ```dart import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'user.ack.dart'; part 'user.ack.g.dart'; -@AckInfer() +@Schemable() final userSchema = Ack.object({ 'name': Ack.string(), 'email': Ack.string().email(), }); -@AckModel() -final class Account with _$AccountAck { - const Account({required this.name}); - final String name; - +@Schemable() +final class Account({required final String name}) with _$AccountAck { static final fromJson = AccountSchema.fromJson; } ``` @@ -89,4 +83,7 @@ Import `package:ack/ack.dart` and you're ready — the [Quickstart Tutorial](./q ## Requirements - Dart SDK: `>=3.9.0 <4.0.0` for core `ack` -- Dart SDK: `>=3.9.0 <4.0.0` when using `ack_annotations` / `ack_generator` +- Dart SDK: `>=3.13.0 <4.0.0` for `ack_generator`, including primary constructors + +Existing applications may keep `ack_annotations`; it re-exports the annotation +API for compatibility. diff --git a/example/lib/additional_properties_example.ack.dart b/example/lib/additional_properties_example.ack.dart index b76c8f43..ce11396a 100644 --- a/example/lib/additional_properties_example.ack.dart +++ b/example/lib/additional_properties_example.ack.dart @@ -11,7 +11,7 @@ part of 'additional_properties_example.dart'; // ************************************************************************** /// Immutable model generated from `userConfigSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class UserConfig { /// Creates a model without validating it. /// @@ -162,7 +162,7 @@ final class _$UserConfigCopyWith implements $UserConfigCopyWith { } /// Immutable model generated from `apiRequestSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class ApiRequest { /// Creates a model without validating it. /// @@ -313,7 +313,7 @@ final class _$ApiRequestCopyWith implements $ApiRequestCopyWith { } /// Immutable model generated from `featureFlagsSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class FeatureFlags { /// Creates a model without validating it. /// @@ -466,7 +466,7 @@ final class _$FeatureFlagsCopyWith } /// Immutable model generated from `dynamicDataSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class DynamicData { /// Creates a model without validating it. /// diff --git a/example/lib/additional_properties_example.dart b/example/lib/additional_properties_example.dart index 9ede4514..4c50d0a8 100644 --- a/example/lib/additional_properties_example.dart +++ b/example/lib/additional_properties_example.dart @@ -3,7 +3,6 @@ library; import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'additional_properties_example.ack.dart'; part 'additional_properties_example.ack.g.dart'; @@ -11,7 +10,7 @@ part 'additional_properties_example.ack.g.dart'; /// Example 1: User configuration with additional metadata /// The generated model has `additionalProperties`, which contains /// only the additional properties (not 'username' or 'email') -@AckInfer() +@Schemable() final userConfigSchema = Ack.object({ 'username': Ack.string(), 'email': Ack.string(), @@ -19,7 +18,7 @@ final userConfigSchema = Ack.object({ /// Example 2: API request with explicit additionalProperties /// Same behavior as passthrough(). -@AckInfer() +@Schemable() final apiRequestSchema = Ack.object({ 'method': Ack.string(), 'url': Ack.string(), @@ -27,12 +26,12 @@ final apiRequestSchema = Ack.object({ /// Example 3: Feature flags with base configuration /// Demonstrates filtering out known fields from dynamic properties -@AckInfer() +@Schemable() final featureFlagsSchema = Ack.object({ 'appVersion': Ack.string(), 'environment': Ack.string(), }).passthrough(); /// Example 4: Empty schema with all properties as additional -@AckInfer() +@Schemable() final dynamicDataSchema = Ack.object({}).passthrough(); diff --git a/example/lib/class_first_models.ack.dart b/example/lib/class_first_models.ack.dart index 0a04aea6..0aa3f2a4 100644 --- a/example/lib/class_first_models.ack.dart +++ b/example/lib/class_first_models.ack.dart @@ -95,6 +95,10 @@ final class _$CatCopyWith implements $CatCopyWith { } mixin _$CatAck { + String get id; + + int get lives; + /// Creates a copy of this model with selected fields replaced. $CatCopyWith get copyWith => _$CatCopyWith(this as Cat); @@ -227,6 +231,10 @@ final class _$DogCopyWith implements $DogCopyWith { } mixin _$DogAck { + String get id; + + String get breed; + /// Creates a copy of this model with selected fields replaced. $DogCopyWith get copyWith => _$DogCopyWith(this as Dog); @@ -454,6 +462,12 @@ final class _$AccountCopyWith implements $AccountCopyWith { } mixin _$AccountAck { + String get displayName; + + Uri? get website; + + String get role; + /// Creates a copy of this model with selected fields replaced. $AccountCopyWith get copyWith => _$AccountCopyWith(this as Account); diff --git a/example/lib/class_first_models.dart b/example/lib/class_first_models.dart index 6c07f3e9..319d5487 100644 --- a/example/lib/class_first_models.dart +++ b/example/lib/class_first_models.dart @@ -1,11 +1,10 @@ import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'class_first_models.ack.dart'; part 'class_first_models.ack.g.dart'; /// A signed-in user's account. -@AckModel( +@Schemable( caseStyle: AckCaseStyle.snake, description: "A signed-in user's account.", ) @@ -19,8 +18,11 @@ final class Account with _$AccountAck { /// The name shown to other users. @MinLength(2) @AckField(description: 'The name shown to other users.') + @override final String displayName; + @override final Uri? website; + @override final String role; static final fromJson = AccountSchema.fromJson; @@ -28,7 +30,7 @@ final class Account with _$AccountAck { /// A pet, identified by its `type`. /// @description A pet, identified by its `type`. -@AckModel(discriminatorKey: 'type') +@Schemable(discriminatorKey: 'type') sealed class Pet with _$PetAck { const Pet({required this.id}); @@ -37,17 +39,19 @@ sealed class Pet with _$PetAck { /// A cat with a limited number of lives. /// @description A cat with a limited number of lives. -@AckModel(discriminatorValue: 'cat') +@Schemable(discriminatorValue: 'cat') final class Cat extends Pet with _$CatAck { const Cat({required super.id, required this.lives}); @Min(1) @Max(9) + @override final int lives; } final class Dog extends Pet with _$DogAck { const Dog({required super.id, required this.breed}); + @override final String breed; } diff --git a/example/lib/pet.ack.dart b/example/lib/pet.ack.dart index dad42c0b..cfd86e86 100644 --- a/example/lib/pet.ack.dart +++ b/example/lib/pet.ack.dart @@ -68,7 +68,7 @@ sealed class Pet { } /// Discriminated model branch generated from `catSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Cat extends Pet { /// Creates a model without validating it. /// @@ -159,7 +159,7 @@ final class _$CatCopyWith implements $CatCopyWith { } /// Discriminated model branch generated from `dogSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Dog extends Pet { /// Creates a model without validating it. /// diff --git a/example/lib/pet.dart b/example/lib/pet.dart index 4eae7a50..0293df70 100644 --- a/example/lib/pet.dart +++ b/example/lib/pet.dart @@ -1,23 +1,22 @@ import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'pet.ack.dart'; part 'pet.ack.g.dart'; /// Pet schemas: discriminated by 'type' -@AckInfer() +@Schemable() final catSchema = Ack.object({ 'type': Ack.literal('cat'), 'lives': Ack.integer().min(1).max(9), }); -@AckInfer() +@Schemable() final dogSchema = Ack.object({ 'type': Ack.literal('dog'), 'breed': Ack.string().minLength(1), }); -@AckInfer() +@Schemable() final petSchema = Ack.discriminated( discriminatorKey: 'type', schemas: {'cat': catSchema, 'dog': dogSchema}, diff --git a/example/lib/schema_types_discriminated.ack.dart b/example/lib/schema_types_discriminated.ack.dart index b5a5de78..fde07106 100644 --- a/example/lib/schema_types_discriminated.ack.dart +++ b/example/lib/schema_types_discriminated.ack.dart @@ -68,7 +68,7 @@ sealed class Pet { } /// Discriminated model branch generated from `catSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Cat extends Pet { /// Creates a model without validating it. /// @@ -159,7 +159,7 @@ final class _$CatCopyWith implements $CatCopyWith { } /// Discriminated model branch generated from `dogSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Dog extends Pet { /// Creates a model without validating it. /// diff --git a/example/lib/schema_types_discriminated.dart b/example/lib/schema_types_discriminated.dart index 84572897..035a2b14 100644 --- a/example/lib/schema_types_discriminated.dart +++ b/example/lib/schema_types_discriminated.dart @@ -1,17 +1,16 @@ import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'schema_types_discriminated.ack.dart'; part 'schema_types_discriminated.ack.g.dart'; /// Discriminated schema example for immutable model generation with @AckInfer. -@AckInfer() +@Schemable() final catSchema = Ack.object({'lives': Ack.integer()}); -@AckInfer() +@Schemable() ObjectSchema get dogSchema => Ack.object({'bark': Ack.boolean()}).passthrough(); -@AckInfer() +@Schemable() final petSchema = Ack.discriminated( discriminatorKey: 'kind', schemas: {'cat': catSchema, 'dog': dogSchema}, diff --git a/example/lib/schema_types_edge_cases.ack.dart b/example/lib/schema_types_edge_cases.ack.dart index dea2612b..6e987250 100644 --- a/example/lib/schema_types_edge_cases.ack.dart +++ b/example/lib/schema_types_edge_cases.ack.dart @@ -11,7 +11,7 @@ part of 'schema_types_edge_cases.dart'; // ************************************************************************** /// Immutable model generated from `productSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Product { /// Creates a model without validating it. /// @@ -163,7 +163,7 @@ final class _$ProductCopyWith implements $ProductCopyWith { } /// Immutable model generated from `gridSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Grid { /// Creates a model without validating it. /// @@ -274,7 +274,7 @@ final class _$GridCopyWith implements $GridCopyWith { } /// Immutable model generated from `addressSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Address { /// Creates a model without validating it. /// @@ -418,7 +418,7 @@ final class _$AddressCopyWith implements $AddressCopyWith
{ } /// Immutable model generated from `personSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Person { /// Creates a model without validating it. /// @@ -554,7 +554,7 @@ final class _$PersonCopyWith implements $PersonCopyWith { } /// Immutable model generated from `employeeSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Employee { /// Creates a model without validating it. /// @@ -702,7 +702,7 @@ final class _$EmployeeCopyWith implements $EmployeeCopyWith { } /// Immutable model generated from `modifierSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Modifier { /// Creates a model without validating it. /// @@ -881,7 +881,7 @@ final class _$ModifierCopyWith implements $ModifierCopyWith { } /// Immutable model generated from `taggedItemSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class TaggedItem { /// Creates a model without validating it. /// @@ -1076,7 +1076,7 @@ final class _$TaggedItemCopyWith implements $TaggedItemCopyWith { } /// Immutable model generated from `contactListSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class ContactList { /// Creates a model without validating it. /// @@ -1191,7 +1191,7 @@ final class _$ContactListCopyWith implements $ContactListCopyWith { } /// Immutable model generated from `emptySchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Empty { /// Creates a model without validating it. /// @@ -1276,7 +1276,7 @@ final class _$EmptyCopyWith implements $EmptyCopyWith { } /// Immutable model generated from `minimalSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Minimal { /// Creates a model without validating it. /// @@ -1372,7 +1372,7 @@ final class _$MinimalCopyWith implements $MinimalCopyWith { } /// Immutable model generated from `namedItemSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class NamedItem { /// Creates a model without validating it. /// @@ -1468,7 +1468,7 @@ final class _$NamedItemCopyWith implements $NamedItemCopyWith { } /// Immutable model generated from `item`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Item { /// Creates a model without validating it. /// @@ -1563,7 +1563,7 @@ final class _$ItemCopyWith implements $ItemCopyWith { } /// Immutable model generated from `myCustomSchema123`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class MyCustomSchema123 { /// Creates a model without validating it. /// diff --git a/example/lib/schema_types_edge_cases.dart b/example/lib/schema_types_edge_cases.dart index fa038dcf..16394ee4 100644 --- a/example/lib/schema_types_edge_cases.dart +++ b/example/lib/schema_types_edge_cases.dart @@ -10,7 +10,6 @@ library; import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'schema_types_edge_cases.ack.dart'; part 'schema_types_edge_cases.ack.g.dart'; @@ -25,7 +24,7 @@ part 'schema_types_edge_cases.ack.g.dart'; /// - tags field should be `List`, not `List` /// - scores field should be `List`, not `List` /// - flags field should be `List`, not `List` -@AckInfer() +@Schemable() final productSchema = Ack.object({ 'name': Ack.string(), 'tags': Ack.list(Ack.string()), // Generates: final List tags @@ -37,7 +36,7 @@ final productSchema = Ack.object({ /// /// EXPECTED BEHAVIOR: /// - matrix field should be `List>` -@AckInfer() +@Schemable() final gridSchema = Ack.object({ 'name': Ack.string(), 'matrix': Ack.list( @@ -50,7 +49,7 @@ final gridSchema = Ack.object({ // ============================================================================ /// Simple address schema for composition -@AckInfer() +@Schemable() final addressSchema = Ack.object({ 'street': Ack.string(), 'city': Ack.string(), @@ -63,7 +62,7 @@ final addressSchema = Ack.object({ /// EXPECTED BEHAVIOR: /// - address field should NOT be null/missing /// - Generates: `final Address address` -@AckInfer() +@Schemable() final personSchema = Ack.object({ 'name': Ack.string(), 'email': Ack.string(), @@ -75,7 +74,7 @@ final personSchema = Ack.object({ /// /// EXPECTED BEHAVIOR: /// - Both homeAddress and workAddress fields should be present -@AckInfer() +@Schemable() final employeeSchema = Ack.object({ 'name': Ack.string(), 'employeeId': Ack.string(), @@ -94,7 +93,7 @@ final employeeSchema = Ack.object({ /// - nullableField: isRequired=true, isNullable=true /// - optionalNullable: isRequired=false, isNullable=true /// - requiredField: isRequired=true, isNullable=false -@AckInfer() +@Schemable() final modifierSchema = Ack.object({ 'requiredField': Ack.string(), 'optionalField': Ack.string().optional(), @@ -112,7 +111,7 @@ final modifierSchema = Ack.object({ /// EXPECTED BEHAVIOR: /// - optionalTags: `List?`, isRequired=false /// - requiredTags: `List`, isRequired=true -@AckInfer() +@Schemable() final taggedItemSchema = Ack.object({ 'name': Ack.string(), 'requiredTags': Ack.list(Ack.string()), @@ -124,7 +123,7 @@ final taggedItemSchema = Ack.object({ /// /// EXPECTED BEHAVIOR: /// - addresses: `List
` (eager list with typed elements) -@AckInfer() +@Schemable() final contactListSchema = Ack.object({ 'name': Ack.string(), 'addresses': Ack.list(addressSchema), @@ -139,11 +138,11 @@ final contactListSchema = Ack.object({ /// EXPECTED BEHAVIOR: /// Generates an immutable model with no stored schema fields. /// - parse() should still work -@AckInfer() +@Schemable() final emptySchema = Ack.object({}); /// Single field schema (minimal case) -@AckInfer() +@Schemable() final minimalSchema = Ack.object({'id': Ack.string()}); // ============================================================================ @@ -151,13 +150,13 @@ final minimalSchema = Ack.object({'id': Ack.string()}); // ============================================================================ /// Schema with 'Schema' suffix (generates `NamedItem`) -@AckInfer() +@Schemable() final namedItemSchema = Ack.object({'name': Ack.string()}); /// Schema without 'Schema' suffix (generates `Item`) -@AckInfer() +@Schemable() final item = Ack.object({'id': Ack.string()}); /// Schema with unusual name (should handle gracefully) -@AckInfer() +@Schemable() final myCustomSchema123 = Ack.object({'value': Ack.string()}); diff --git a/example/lib/schema_types_primitives.ack.dart b/example/lib/schema_types_primitives.ack.dart index 6b8785e5..452bcf2d 100644 --- a/example/lib/schema_types_primitives.ack.dart +++ b/example/lib/schema_types_primitives.ack.dart @@ -11,7 +11,7 @@ part of 'schema_types_primitives.dart'; // ************************************************************************** /// Immutable value model generated from `passwordSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Password { /// Creates a model without validating it. /// @@ -106,7 +106,7 @@ final class _$PasswordCopyWith implements $PasswordCopyWith { } /// Immutable value model generated from `ageSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Age { /// Creates a model without validating it. /// @@ -200,7 +200,7 @@ final class _$AgeCopyWith implements $AgeCopyWith { } /// Immutable value model generated from `priceSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Price { /// Creates a model without validating it. /// @@ -294,7 +294,7 @@ final class _$PriceCopyWith implements $PriceCopyWith { } /// Immutable value model generated from `activeSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Active { /// Creates a model without validating it. /// @@ -388,7 +388,7 @@ final class _$ActiveCopyWith implements $ActiveCopyWith { } /// Immutable value model generated from `tagsSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Tags { /// Creates a model without validating it. /// @@ -485,7 +485,7 @@ final class _$TagsCopyWith implements $TagsCopyWith { } /// Immutable value model generated from `scoresSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Scores { /// Creates a model without validating it. /// @@ -582,7 +582,7 @@ final class _$ScoresCopyWith implements $ScoresCopyWith { } /// Immutable value model generated from `statusSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class StatusLiteral { /// Creates a model without validating it. /// @@ -679,7 +679,7 @@ final class _$StatusLiteralCopyWith } /// Immutable value model generated from `roleSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Role { /// Creates a model without validating it. /// @@ -773,7 +773,7 @@ final class _$RoleCopyWith implements $RoleCopyWith { } /// Immutable value model generated from `userRoleSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class UserRoleModel { /// Creates a model without validating it. /// @@ -871,7 +871,7 @@ final class _$UserRoleModelCopyWith } /// Immutable value model generated from `statusEnumSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class StatusEnum { /// Creates a model without validating it. /// @@ -966,7 +966,7 @@ final class _$StatusEnumCopyWith implements $StatusEnumCopyWith { } /// Immutable value model generated from `optionalStatusSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class OptionalStatus { /// Creates a model without validating it. /// @@ -1064,7 +1064,7 @@ final class _$OptionalStatusCopyWith } /// Immutable value model generated from `defaultedEnumSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class DefaultedEnum { /// Creates a model without validating it. /// @@ -1162,7 +1162,7 @@ final class _$DefaultedEnumCopyWith } /// Immutable value model generated from `chainedEnumStringSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class ChainedEnumString { /// Creates a model without validating it. /// @@ -1260,7 +1260,7 @@ final class _$ChainedEnumStringCopyWith } /// Immutable value model generated from `refinedAgeSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class RefinedAge { /// Creates a model without validating it. /// diff --git a/example/lib/schema_types_primitives.dart b/example/lib/schema_types_primitives.dart index 803794cd..e26c706b 100644 --- a/example/lib/schema_types_primitives.dart +++ b/example/lib/schema_types_primitives.dart @@ -1,5 +1,4 @@ import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'schema_types_primitives.ack.dart'; part 'schema_types_primitives.ack.g.dart'; @@ -15,68 +14,64 @@ enum UserRole { admin, user, guest } enum Status { active, inactive, pending } // String schema -@AckInfer() +@Schemable() final passwordSchema = Ack.string().minLength(8); // Integer schema -@AckInfer() +@Schemable() final ageSchema = Ack.integer().min(0).max(150); // Double schema -@AckInfer() +@Schemable() final priceSchema = Ack.double().min(0); // Boolean schema -@AckInfer() +@Schemable() final activeSchema = Ack.boolean(); // List schema -@AckInfer() +@Schemable() final tagsSchema = Ack.list(Ack.string()); // List of integers -@AckInfer() +@Schemable() final scoresSchema = Ack.list(Ack.integer()); // Literal schema -@AckInfer(name: 'StatusLiteral') +@Schemable(name: 'StatusLiteral') final statusSchema = Ack.literal('active'); // String enum schema -@AckInfer() +@Schemable() final roleSchema = Ack.enumString(['admin', 'user', 'guest']); // EnumValues schemas -@AckInfer(name: 'UserRoleModel') +@Schemable(name: 'UserRoleModel') final userRoleSchema = Ack.enumValues(UserRole.values); -@AckInfer() +@Schemable() final statusEnumSchema = Ack.enumValues(Status.values); // Method chaining tests for new schema types -@AckInfer() +@Schemable() final optionalStatusSchema = Ack.literal('active').optional(); final nullableRoleSchema = Ack.enumString(['admin', 'user']).nullable(); -@AckInfer() -final defaultedEnumSchema = Ack.enumValues( - UserRole.values, -).withDefault(UserRole.guest); +@Schemable() +final defaultedEnumSchema = Ack.enumValues(UserRole.values) + .withDefault(UserRole.guest); -final optionalNullableLiteralSchema = Ack.literal( - 'pending', -).optional().nullable(); +final optionalNullableLiteralSchema = Ack.literal('pending') + .optional() + .nullable(); -@AckInfer() -final chainedEnumStringSchema = Ack.enumString([ - 'read', - 'write', - 'execute', -]).withDefault('read'); +@Schemable() +final chainedEnumStringSchema = Ack.enumString(['read', 'write', 'execute']) + .withDefault('read'); // Test refine - this should work -@AckInfer() +@Schemable() final refinedAgeSchema = Ack.integer() .min(0) .refine((age) => age < 150, message: 'Age must be less than 150'); diff --git a/example/lib/schema_types_simple.ack.dart b/example/lib/schema_types_simple.ack.dart index d87a21f4..08b48c00 100644 --- a/example/lib/schema_types_simple.ack.dart +++ b/example/lib/schema_types_simple.ack.dart @@ -11,7 +11,7 @@ part of 'schema_types_simple.dart'; // ************************************************************************** /// Immutable model generated from `userSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class User { /// Creates a model without validating it. /// diff --git a/example/lib/schema_types_simple.dart b/example/lib/schema_types_simple.dart index 346991b4..4d154a62 100644 --- a/example/lib/schema_types_simple.dart +++ b/example/lib/schema_types_simple.dart @@ -1,11 +1,10 @@ import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'schema_types_simple.ack.dart'; part 'schema_types_simple.ack.g.dart'; /// Simple example: Basic primitives -@AckInfer() +@Schemable() final userSchema = Ack.object({ 'name': Ack.string(), 'age': Ack.integer(), diff --git a/example/lib/schema_types_transforms.ack.dart b/example/lib/schema_types_transforms.ack.dart index 3eb9daae..612c1623 100644 --- a/example/lib/schema_types_transforms.ack.dart +++ b/example/lib/schema_types_transforms.ack.dart @@ -11,7 +11,7 @@ part of 'schema_types_transforms.dart'; // ************************************************************************** /// Immutable value model generated from `colorSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class ColorModel { /// Creates a model without validating it. /// @@ -106,7 +106,7 @@ final class _$ColorModelCopyWith implements $ColorModelCopyWith { } /// Immutable model generated from `profileSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Profile { /// Creates a model without validating it. /// diff --git a/example/lib/schema_types_transforms.dart b/example/lib/schema_types_transforms.dart index a103a0e9..2dc4521d 100644 --- a/example/lib/schema_types_transforms.dart +++ b/example/lib/schema_types_transforms.dart @@ -1,5 +1,4 @@ import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'schema_types_transforms.ack.dart'; part 'schema_types_transforms.ack.g.dart'; @@ -16,13 +15,13 @@ class TagList { final baseColorSchema = Ack.string(); -@AckInfer(name: 'ColorModel') +@Schemable(name: 'ColorModel') final colorSchema = Ack.string().codec( decode: Color.new, encode: (color) => color.value, ); -@AckInfer() +@Schemable() final profileSchema = Ack.object({ 'homepage': Ack.uri(), 'birthday': Ack.date(), @@ -46,7 +45,6 @@ final profileSchema = Ack.object({ encode: (color) => color.value, ), ), - 'tagList': Ack.list( - Ack.string(), - ).codec(decode: TagList.new, encode: (tags) => tags.value), + 'tagList': Ack.list(Ack.string()) + .codec(decode: TagList.new, encode: (tags) => tags.value), }); diff --git a/example/lib/user_with_color.ack.dart b/example/lib/user_with_color.ack.dart index 116774ec..79e3e561 100644 --- a/example/lib/user_with_color.ack.dart +++ b/example/lib/user_with_color.ack.dart @@ -11,7 +11,7 @@ part of 'user_with_color.dart'; // ************************************************************************** /// Immutable value model generated from `colorSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class ColorModel { /// Creates a model without validating it. /// @@ -106,7 +106,7 @@ final class _$ColorModelCopyWith implements $ColorModelCopyWith { } /// Immutable model generated from `profileSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class Profile { /// Creates a model without validating it. /// @@ -222,7 +222,7 @@ final class _$ProfileCopyWith implements $ProfileCopyWith { } /// Immutable model generated from `userWithColorSchema`. -@AckInfer.jsonSerializable +@Schemable.generatedJson final class UserWithColor { /// Creates a model without validating it. /// diff --git a/example/lib/user_with_color.dart b/example/lib/user_with_color.dart index 8a9b03f5..34cbed47 100644 --- a/example/lib/user_with_color.dart +++ b/example/lib/user_with_color.dart @@ -1,5 +1,4 @@ import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; import 'pet.dart'; @@ -16,7 +15,7 @@ class Color { } /// Color schema: validates and bidirectionally maps hex values to Color. -@AckInfer(name: 'ColorModel') +@Schemable(name: 'ColorModel') final colorSchema = Ack.string() .refine( (value) => RegExp(r'^#[0-9a-fA-F]{6}$').hasMatch(value), @@ -28,14 +27,14 @@ final colorSchema = Ack.string() ); /// Profile: nested object with bio and website -@AckInfer() +@Schemable() final profileSchema = Ack.object({ 'bio': Ack.string().minLength(1).maxLength(500), 'website': Ack.uri().optional(), }); /// User with color: combines user fields, nested profile, and color -@AckInfer() +@Schemable() final userWithColorSchema = Ack.object({ 'firstName': Ack.string().minLength(1).maxLength(50), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 7ac8d586..ae1de867 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -6,11 +6,10 @@ publish_to: none resolution: workspace environment: - sdk: '>=3.9.0 <4.0.0' + sdk: '>=3.13.0 <4.0.0' dependencies: ack: ^1.2.0 - ack_annotations: ^1.2.0 dev_dependencies: ack_generator: ^1.2.0 diff --git a/example/pubspec_overrides.yaml b/example/pubspec_overrides.yaml index c437b57c..57065b18 100644 --- a/example/pubspec_overrides.yaml +++ b/example/pubspec_overrides.yaml @@ -1,8 +1,6 @@ -# # melos_managed_dependency_overrides: ack,ack_annotations,ack_generator +# # melos_managed_dependency_overrides: ack,ack_generator # dependency_overrides: # ack: # path: ../packages/ack -# ack_annotations: -# path: ../packages/ack_annotations # ack_generator: # path: ../packages/ack_generator diff --git a/llms.txt b/llms.txt index 29db1bc9..8b5b4592 100644 --- a/llms.txt +++ b/llms.txt @@ -3,18 +3,18 @@ > A schema validation library for Dart and Flutter with a fluent runtime API, bidirectional codecs, and two-way model/schema code generation. Ack validates untrusted boundary data with schemas built through the `Ack` -factory. Annotate a schema with `@AckInfer()` to generate a model, or annotate a -hand-written class with `@AckModel()` to generate its schema and JSON helpers. +factory. Annotate a schema or hand-written class with `@Schemable()` to generate its model, schema, and JSON helpers. There is no `@AckSchema()` annotation; `AckSchema` is the runtime schema type. ## Packages 1. `ack`: core runtime validation, codecs, and generated-model support -2. `ack_annotations`: exposes `@AckInfer()` and `@AckModel()` -3. `ack_generator`: generates models from schemas and schemas from models -4. `ack_firebase_ai`: converts Ack schemas to Firebase AI structured-output schemas -5. `ack_json_schema_builder`: converts Ack schemas to `json_schema_builder` schemas -6. `ack_mcp_dart`: advertises MCP tool input schemas and validates typed arguments with `mcp_dart` +2. `ack`: owns `@Schemable()`, `@Schemable()`, and `@Schemable()` annotations +3. `ack_annotations`: compatibility re-exports for the moved annotations +4. `ack_generator`: generates models from schemas and schemas from models +5. `ack_firebase_ai`: converts Ack schemas to Firebase AI structured-output schemas +6. `ack_json_schema_builder`: converts Ack schemas to `json_schema_builder` schemas +7. `ack_mcp_dart`: advertises MCP tool input schemas and validates typed arguments with `mcp_dart` ## Core runtime usage @@ -48,25 +48,24 @@ Codecs decode a boundary value into a runtime value and encode it back. ## Model code generation -Both directions use the same generated parts and build command. `@AckInfer()` -is schema-first; `@AckModel()` is class-first. +Both directions use `@Schemable()` and the same generated parts and build command. +`@AckInfer()` and `@AckModel()` remain deprecated compatibility annotations. Every annotated library declares both generated parts: ```dart import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'user_schema.ack.dart'; part 'user_schema.ack.g.dart'; -@AckInfer() +@Schemable() final addressSchema = Ack.object({ 'street': Ack.string(), 'city': Ack.string(), }); -@AckInfer() +@Schemable() final userSchema = Ack.object({ 'name': Ack.string(), 'address': addressSchema, @@ -74,7 +73,7 @@ final userSchema = Ack.object({ ``` Run `dart run build_runner build`. `addressSchema` and `userSchema` generate -`Address` and `User`. A custom `@AckInfer(name: 'Member')` value is used exactly. +`Address` and `User`. A custom `@Schemable(name: 'Member')` value is used exactly. Generated object models provide: @@ -93,7 +92,7 @@ Generated object models provide: For class-first generation: ```dart -@AckModel(caseStyle: AckCaseStyle.snake) +@Schemable(caseStyle: AckCaseStyle.snake) final class Account with _$AccountAck { const Account({required this.displayName, this.role = 'member'}); @@ -113,11 +112,11 @@ equality. The facade exposes `schema`, `wireSchema`, `parse`, `safeParse`, `schemaName:` overrides the exact public UpperCamelCase facade name; no public lower-camel alias is emitted. Constructor parameters determine presence and defaults. Field types and constraint annotations determine the schema. Sealed -classes use `@AckModel(discriminatorKey: ...)`; same-library concrete branches +classes use `@Schemable(discriminatorKey: ...)`; same-library concrete branches are included automatically. Use `@Optional()` or `@Required()` to override inferred key presence, `@NotNull()` to reject an explicit JSON `null` without requiring the key, and `@AckField(schema: ...)` for custom field codecs. -Use `@AckModel(description: ...)` and `@AckField(description: ...)` to export +Use `@Schemable(description: ...)` and `@AckField(description: ...)` to export schema descriptions. A single-line `@description` doc tag is a fallback. Untagged prose does not export. A field description applies at its property without changing a nested model's own description. @@ -162,7 +161,8 @@ Generation rejects nullable roots, one-way transforms, `Ack.any()` and object fields, non-string map keys, unresolved dynamic factories, name collisions, and cross-library discriminated branches. -Class-first generation supports object models, inferred scalar and enum +Class-first generation supports object models, redirecting private factory +implementations, inferred scalar and enum fields, JSON-safe `Object` fields, nested lists, sets through list codecs, string-keyed maps, custom `@AckField` schemas, `@Optional()`, `@Required()`, `@NotNull()`, constructor defaults, case styles, `AckUnknownPropertyPolicy`, @@ -186,23 +186,23 @@ maps), not an arbitrary Dart instance: ## Discriminated generated models -`Ack.discriminated(...)` works with `@AckInfer()` when: +`Ack.discriminated(...)` works with `@Schemable()` when: - `schemas` is a non-empty map literal; -- each branch is a top-level `@AckInfer()` object schema in the same library; +- each branch is a top-level `@Schemable()` object schema in the same library; - each branch is non-nullable; - branch schemas normally omit the discriminator field; - an included discriminator is an exact matching `Ack.literal(...)` or an `Ack.enumString(...)` containing the branch key. ```dart -@AckInfer() +@Schemable() final catSchema = Ack.object({'lives': Ack.integer()}); -@AckInfer() +@Schemable() final dogSchema = Ack.object({'breed': Ack.string()}); -@AckInfer() +@Schemable() final petSchema = Ack.discriminated( discriminatorKey: 'type', schemas: {'cat': catSchema, 'dog': dogSchema}, @@ -215,7 +215,7 @@ validates through the union's effective branch. ## Migration from the previous generator -- Rename `@AckType()` to `@AckInfer()` for each connected graph you opt in. +- Rename `@AckType()` to `@Schemable()` for each connected graph you opt in. - Add both `.ack.dart` and `.ack.g.dart` part directives. - Rename generated `UserType` usages to `User` unless a custom name is set. - Replace map/list/scalar interface access with stored fields or `.value`. diff --git a/packages/ack/CHANGELOG.md b/packages/ack/CHANGELOG.md index 9ea99114..eef5f0f9 100644 --- a/packages/ack/CHANGELOG.md +++ b/packages/ack/CHANGELOG.md @@ -1,3 +1,16 @@ +## Unreleased + +### Changed + +* Use `@Schemable()` for schema-first and class-first generation. Deprecate + `@AckInfer()` and `@AckModel()` while retaining their behavior. + +### Added + +* Own the schema-generation annotations and export them from + `package:ack/annotations.dart`. `ack_annotations` remains a compatibility + re-export package. + ## 1.7.0-beta.2 ### Added diff --git a/packages/ack/lib/ack.dart b/packages/ack/lib/ack.dart index 558525df..bb87c7b7 100644 --- a/packages/ack/lib/ack.dart +++ b/packages/ack/lib/ack.dart @@ -45,3 +45,5 @@ export 'src/validation/ack_exception.dart'; export 'src/validation/schema_error.dart'; // Validation results export 'src/validation/schema_result.dart'; +// Code generation annotations +export 'annotations.dart'; diff --git a/packages/ack/lib/ack_generator_support.dart b/packages/ack/lib/ack_generator_support.dart new file mode 100644 index 00000000..05a0f368 --- /dev/null +++ b/packages/ack/lib/ack_generator_support.dart @@ -0,0 +1,4 @@ +/// Internal support types used by `ack_generator`. +library; + +export 'src/annotations/ack_generated_json.dart'; diff --git a/packages/ack/lib/annotations.dart b/packages/ack/lib/annotations.dart new file mode 100644 index 00000000..9d8b1b32 --- /dev/null +++ b/packages/ack/lib/annotations.dart @@ -0,0 +1,10 @@ +/// User-facing annotations for Ack schema generation. +library; + +export 'src/annotations/ack_field.dart'; +export 'src/annotations/ack_infer.dart'; +export 'src/annotations/ack_model.dart'; +export 'src/annotations/ack_type.dart'; +export 'src/annotations/constraints.dart'; +export 'src/annotations/schemable.dart'; +export 'package:json_annotation/json_annotation.dart' show JsonKey; diff --git a/packages/ack/lib/format_annotations.dart b/packages/ack/lib/format_annotations.dart new file mode 100644 index 00000000..d7419451 --- /dev/null +++ b/packages/ack/lib/format_annotations.dart @@ -0,0 +1,4 @@ +/// Format annotations that conflict with Dart core type names. +library; + +export 'src/annotations/format_constraints.dart'; diff --git a/packages/ack_annotations/lib/src/ack_field.dart b/packages/ack/lib/src/annotations/ack_field.dart similarity index 100% rename from packages/ack_annotations/lib/src/ack_field.dart rename to packages/ack/lib/src/annotations/ack_field.dart diff --git a/packages/ack_annotations/lib/src/ack_generated_json.dart b/packages/ack/lib/src/annotations/ack_generated_json.dart similarity index 93% rename from packages/ack_annotations/lib/src/ack_generated_json.dart rename to packages/ack/lib/src/annotations/ack_generated_json.dart index 4ef87690..2dc907e4 100644 --- a/packages/ack_annotations/lib/src/ack_generated_json.dart +++ b/packages/ack/lib/src/annotations/ack_generated_json.dart @@ -3,7 +3,7 @@ import 'package:json_annotation/json_annotation.dart'; /// Generator-support marker applied to Ack-generated model classes. /// /// This is not part of the user-facing annotation API. `ack_generator` emits -/// [AckInfer.jsonSerializable] so the internal JSON builder can delegate +/// `Schemable.generatedJson` so the internal JSON builder can delegate /// structural mapping to `json_serializable` without a literal /// `@JsonSerializable` annotation that the ordinary builder would also claim. /// diff --git a/packages/ack_annotations/lib/src/ack_infer.dart b/packages/ack/lib/src/annotations/ack_infer.dart similarity index 97% rename from packages/ack_annotations/lib/src/ack_infer.dart rename to packages/ack/lib/src/annotations/ack_infer.dart index e8f3a18a..bc9a0d11 100644 --- a/packages/ack_annotations/lib/src/ack_infer.dart +++ b/packages/ack/lib/src/annotations/ack_infer.dart @@ -31,6 +31,7 @@ import 'ack_generated_json.dart'; /// - Classes /// - Instance members /// - Local variables +@Deprecated('Use @Schemable() for schema-first generation.') @Target({TargetKind.topLevelVariable, TargetKind.getter}) final class AckInfer { /// Internal marker used on generated model classes. diff --git a/packages/ack_annotations/lib/src/ack_model.dart b/packages/ack/lib/src/annotations/ack_model.dart similarity index 98% rename from packages/ack_annotations/lib/src/ack_model.dart rename to packages/ack/lib/src/annotations/ack_model.dart index dbb78bcf..ef750ce2 100644 --- a/packages/ack_annotations/lib/src/ack_model.dart +++ b/packages/ack/lib/src/annotations/ack_model.dart @@ -26,6 +26,7 @@ enum AckUnknownPropertyPolicy { reject, discard, capture } /// implicit sealed-union branches must be `final class` declarations, use only /// final stored fields, and apply the generated `_$ClassAck` mixin. Annotated /// sealed union bases remain supported. +@Deprecated('Use @Schemable() for class-first generation.') @Target({TargetKind.classType}) final class AckModel { /// Creates a class-first Ack model annotation. diff --git a/packages/ack_annotations/lib/src/ack_type.dart b/packages/ack/lib/src/annotations/ack_type.dart similarity index 100% rename from packages/ack_annotations/lib/src/ack_type.dart rename to packages/ack/lib/src/annotations/ack_type.dart diff --git a/packages/ack_annotations/lib/src/constraints.dart b/packages/ack/lib/src/annotations/constraints.dart similarity index 100% rename from packages/ack_annotations/lib/src/constraints.dart rename to packages/ack/lib/src/annotations/constraints.dart diff --git a/packages/ack_annotations/lib/src/format_constraints.dart b/packages/ack/lib/src/annotations/format_constraints.dart similarity index 100% rename from packages/ack_annotations/lib/src/format_constraints.dart rename to packages/ack/lib/src/annotations/format_constraints.dart diff --git a/packages/ack/lib/src/annotations/schemable.dart b/packages/ack/lib/src/annotations/schemable.dart new file mode 100644 index 00000000..12129a46 --- /dev/null +++ b/packages/ack/lib/src/annotations/schemable.dart @@ -0,0 +1,79 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta_meta.dart'; + +import 'ack_model.dart'; +import 'ack_generated_json.dart'; + +/// Marks an Ack schema declaration. +/// +/// Use this annotation on a top-level schema variable or getter to generate a +/// schema-first model. Use it on a class to generate a class-first schema. +/// The target determines which options apply. [AckInfer] and [AckModel] +/// remain supported as compatibility spellings. +@Target({TargetKind.classType, TargetKind.topLevelVariable, TargetKind.getter}) +final class Schemable { + /// Internal JSON marker for schema-first generated models. + static const Object generatedJson = AckGeneratedJson(); + + /// Creates a unified schema declaration annotation. + const Schemable({ + this.name, + this.schemaName, + this.description, + this.caseStyle = AckCaseStyle.none, + this.discriminatorKey, + this.discriminatorValue, + this.unknownProperties = AckUnknownPropertyPolicy.reject, + this.captureField = 'additionalProperties', + }) : jsonSerializable = caseStyle == AckCaseStyle.snake + ? const JsonSerializable( + includeIfNull: false, + fieldRename: FieldRename.snake, + ) + : caseStyle == AckCaseStyle.kebab + ? const JsonSerializable( + includeIfNull: false, + fieldRename: FieldRename.kebab, + ) + : caseStyle == AckCaseStyle.pascal + ? const JsonSerializable( + includeIfNull: false, + fieldRename: FieldRename.pascal, + ) + : caseStyle == AckCaseStyle.screamingSnake + ? const JsonSerializable( + includeIfNull: false, + fieldRename: FieldRename.screamingSnake, + ) + : const JsonSerializable( + includeIfNull: false, + fieldRename: FieldRename.none, + ); + + /// Exact generated model name for a top-level schema declaration. + final String? name; + + /// Exact generated schema facade name for a class declaration. + final String? schemaName; + + /// Description for a class schema. Top-level schemas should use `.describe()`. + final String? description; + + /// JSON field-name style for a class declaration. + final AckCaseStyle caseStyle; + + /// Discriminator key for a sealed class declaration. + final String? discriminatorKey; + + /// Discriminator value for a concrete union branch. + final String? discriminatorValue; + + /// Unknown-property policy for a class declaration. + final AckUnknownPropertyPolicy unknownProperties; + + /// Dart field that stores captured unknown properties. + final String captureField; + + /// Fixed JSON mapping configuration for a class declaration. + final JsonSerializable jsonSerializable; +} diff --git a/packages/ack/pubspec.yaml b/packages/ack/pubspec.yaml index af848114..6e869850 100644 --- a/packages/ack/pubspec.yaml +++ b/packages/ack/pubspec.yaml @@ -16,6 +16,7 @@ topics: environment: sdk: '>=3.9.0 <4.0.0' dependencies: + json_annotation: ^4.12.0 meta: ^1.15.0 collection: ^1.18.0 diff --git a/packages/ack/test/documentation/getting_started_installation_examples_test.dart b/packages/ack/test/documentation/getting_started_installation_examples_test.dart index 6c1fdcb4..aa05b435 100644 --- a/packages/ack/test/documentation/getting_started_installation_examples_test.dart +++ b/packages/ack/test/documentation/getting_started_installation_examples_test.dart @@ -27,7 +27,8 @@ void main() { expect(content, contains('@AckModel()')); expect(content, contains('ack_generator')); expect(content, contains('build_runner')); - expect(content, contains('There is no `@AckSchema()`')); + expect(content, contains('@Schemable()')); + expect(content, contains('>=3.13.0 <4.0.0')); }); }); } diff --git a/packages/ack_annotations/CHANGELOG.md b/packages/ack_annotations/CHANGELOG.md index 498b8dc8..475de076 100644 --- a/packages/ack_annotations/CHANGELOG.md +++ b/packages/ack_annotations/CHANGELOG.md @@ -1,3 +1,12 @@ +## Unreleased + +* Move the annotation implementations to `ack`. Existing imports continue to + work through compatibility barrels; new code should import + `package:ack/annotations.dart`. +* Add `@Schemable()` as the unified schema-first and class-first annotation. + Existing `@AckInfer()` and `@AckModel()` spellings remain supported but + deprecated. + ## 1.7.0-beta.2 ### Added diff --git a/packages/ack_annotations/README.md b/packages/ack_annotations/README.md index 273dc0be..f17eb0ad 100644 --- a/packages/ack_annotations/README.md +++ b/packages/ack_annotations/README.md @@ -1,15 +1,17 @@ # ack_annotations -`ack_annotations` provides the `@AckInfer()` schema-first and `@AckModel()` -class-first annotations used by `ack_generator`. Deprecated `@AckType()` is -retained for Ack 1.1 extension-type compatibility. +`ack_annotations` is a compatibility package. Ack now owns the annotations +used by `ack_generator`, including `@Schemable()`, `@AckInfer()`, +`@AckModel()`, and deprecated `@AckType()`. + +New code should depend on `ack` and import `package:ack/annotations.dart`. +This package re-exports the same declarations for existing applications. ## Installation ```yaml dependencies: ack: ^1.7.0-beta.2 - ack_annotations: ^1.7.0-beta.2 dev_dependencies: ack_generator: ^1.7.0-beta.2 @@ -22,12 +24,12 @@ Annotate a top-level Ack schema variable or getter and run `build_runner`: ```dart import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; +import 'package:ack/annotations.dart'; part 'user.ack.dart'; part 'user.ack.g.dart'; -@AckInfer() +@Schemable() final userSchema = Ack.object({ 'name': Ack.string(), 'email': Ack.string().email(), @@ -51,7 +53,7 @@ For class-first generation, keep the class in source and apply its generated mixin: ```dart -@AckModel() +@Schemable() final class Account with _$AccountAck { const Account({required this.name}); @@ -64,13 +66,28 @@ final class Account with _$AccountAck { This generates the public `AccountSchema` facade plus validated `toJson`, `safeToJson`, `copyWith`, equality, and `toString` implementations. -Use `@AckModel(description: ...)` and `@AckField(description: ...)` for schema -descriptions. A single-line `@description` documentation tag is a fallback. + +An abstract class can keep its implementation private with a redirecting +factory. Declare a zero-argument private constructor, apply the generated +mixin, and redirect the unnamed factory with named parameters to `_Class`: + +```dart +@Schemable() +abstract class HabitDto with _$HabitDtoAck { + const HabitDto._(); + const factory HabitDto({required String name}) = _HabitDto; +} +``` + +The generator emits `_HabitDto` and uses the factory parameters as its stored +fields. Factory parameters must be named. Use schema `.describe(...)` calls or +a single-line `@description` documentation tag for descriptions. `@AckField` +remains available for custom codecs and presence overrides. Untagged prose does not become schema data. Explicit annotation text takes precedence over the tag. A field description applies to its property without changing a nested model's description. Format annotations such as `@Url()`, `@Uuid()`, and `@Date()` constrain string -fields. Import `package:ack_annotations/format_annotations.dart` with a +fields. Import `package:ack/format_annotations.dart` with a prefix for `@formats.Uri()` and `@formats.DateTime()`. The prefix preserves the Dart core `Uri` and `DateTime` type names. diff --git a/packages/ack_annotations/example/example.dart b/packages/ack_annotations/example/example.dart index fbfc7b86..d1d17524 100644 --- a/packages/ack_annotations/example/example.dart +++ b/packages/ack_annotations/example/example.dart @@ -5,13 +5,13 @@ // // A real class-first library adds the two part directives and the generated // `_$UserAck` mixin. See the ack_generator package for a complete setup. -import 'package:ack_annotations/ack_annotations.dart'; +import 'package:ack/annotations.dart'; -/// Class-first: `@AckModel()` derives a schema from the constructor. +/// Class-first: `@Schemable()` derives a schema from the constructor. /// /// `caseStyle` renames every JSON key, and the constraint annotations add /// validation that the generated schema enforces. -@AckModel(caseStyle: AckCaseStyle.snake) +@Schemable(caseStyle: AckCaseStyle.snake) final class User { const User({required this.fullName, required this.email, this.age}); @@ -32,7 +32,7 @@ final class User { /// /// [AckUnknownPropertyPolicy.capture] stores them in [captureField] and writes /// them back when the model encodes. -@AckModel( +@Schemable( unknownProperties: AckUnknownPropertyPolicy.capture, captureField: 'extras', ) @@ -46,7 +46,7 @@ final class TolerantPayload { /// `@Required()` and `@Optional()` override what the constructor implies for one /// field. Keep `@AckField(schema: ...)` for custom field codecs. -@AckModel() +@Schemable() final class Article { const Article({required this.title, this.summary}); @@ -60,10 +60,10 @@ final class Article { void main() { // The annotations carry const data that the generator reads at build time. - const user = AckModel(caseStyle: AckCaseStyle.snake); + const user = Schemable(caseStyle: AckCaseStyle.snake); print('User keys use ${user.caseStyle.name} case.'); - const tolerant = AckModel( + const tolerant = Schemable( unknownProperties: AckUnknownPropertyPolicy.capture, captureField: 'extras', ); diff --git a/packages/ack_annotations/lib/ack_annotations.dart b/packages/ack_annotations/lib/ack_annotations.dart index 4b705560..4f43ab70 100644 --- a/packages/ack_annotations/lib/ack_annotations.dart +++ b/packages/ack_annotations/lib/ack_annotations.dart @@ -1,21 +1,4 @@ -/// Annotation library for Ack schema model-class generation. -/// -/// Import this library to use `@AckInfer()` on top-level schema declarations or -/// `@AckModel()` on hand-written model classes processed by `ack_generator`. -/// Deprecated `@AckType()` remains available for Ack 1.1 compatibility. -/// -/// Class-first models apply the generated `_$ClassAck` mixin and may use -/// [AckUnknownPropertyPolicy], `@Optional()`, `@Required()`, `@NotNull()`, -/// and `@AckField(schema: ...)` to describe wire extras, field presence, -/// and custom field codecs. Deprecated [AckFieldPresence] remains available -/// during migration. -/// Import `format_annotations.dart` with a prefix for `@Uri()` and -/// `@DateTime()`, whose names also belong to Dart core types. +/// Compatibility barrel for Ack's moved annotation API. library; -export 'package:json_annotation/json_annotation.dart' show JsonKey; -export 'src/ack_field.dart'; -export 'src/ack_infer.dart'; -export 'src/ack_model.dart'; -export 'src/ack_type.dart'; -export 'src/constraints.dart'; +export 'package:ack/annotations.dart'; diff --git a/packages/ack_annotations/lib/ack_generator_support.dart b/packages/ack_annotations/lib/ack_generator_support.dart index 3aa99c77..0ca71c80 100644 --- a/packages/ack_annotations/lib/ack_generator_support.dart +++ b/packages/ack_annotations/lib/ack_generator_support.dart @@ -1,9 +1,4 @@ -/// Generator-support types for `ack_generator`. -/// -/// Application code should import `package:ack_annotations/ack_annotations.dart` -/// and use `@AckInfer()` or `@AckModel()`. Existing Ack 1.1 code may continue -/// using the deprecated `@AckType()` during migration. This library only -/// exposes the generator's internal JSON marker. +/// Compatibility barrel for Ack generator support types. library; -export 'src/ack_generated_json.dart'; +export 'package:ack/ack_generator_support.dart'; diff --git a/packages/ack_annotations/lib/format_annotations.dart b/packages/ack_annotations/lib/format_annotations.dart index 65c235c6..ff3c9cc9 100644 --- a/packages/ack_annotations/lib/format_annotations.dart +++ b/packages/ack_annotations/lib/format_annotations.dart @@ -1,7 +1,4 @@ -/// Format annotations that conflict with Dart core type names. -/// -/// Import this library with a prefix to use `@formats.DateTime()` and -/// `@formats.Uri()`. +/// Compatibility barrel for Ack format annotations. library; -export 'src/format_constraints.dart'; +export 'package:ack/format_annotations.dart'; diff --git a/packages/ack_annotations/pubspec.yaml b/packages/ack_annotations/pubspec.yaml index b6322993..c890f871 100644 --- a/packages/ack_annotations/pubspec.yaml +++ b/packages/ack_annotations/pubspec.yaml @@ -1,5 +1,5 @@ name: ack_annotations -description: Annotations for Ack schema-first models and class-first schema generation +description: Compatibility re-exports for Ack schema-generation annotations version: 1.7.0-beta.2 repository: https://github.com/btwld/ack resolution: workspace @@ -13,8 +13,7 @@ environment: sdk: '>=3.9.0 <4.0.0' dependencies: - json_annotation: ^4.12.0 - meta: ^1.15.0 + ack: ^1.7.0-beta.2 dev_dependencies: # Every sibling package declares lints, so the shared analysis_options.yaml diff --git a/packages/ack_annotations/test/ack_model_test.dart b/packages/ack_annotations/test/ack_model_test.dart index 4082919f..fd354259 100644 --- a/packages/ack_annotations/test/ack_model_test.dart +++ b/packages/ack_annotations/test/ack_model_test.dart @@ -1,3 +1,6 @@ +// Compatibility tests intentionally use deprecated annotations. +// ignore_for_file: deprecated_member_use + import 'package:ack_annotations/ack_annotations.dart'; import 'package:test/test.dart'; diff --git a/packages/ack_annotations/test/ack_type_test.dart b/packages/ack_annotations/test/ack_type_test.dart index a1a806f0..b3ceb5a1 100644 --- a/packages/ack_annotations/test/ack_type_test.dart +++ b/packages/ack_annotations/test/ack_type_test.dart @@ -1,3 +1,6 @@ +// Compatibility tests intentionally use deprecated annotations. +// ignore_for_file: deprecated_member_use + import 'package:ack_annotations/ack_annotations.dart'; import 'package:ack_annotations/ack_generator_support.dart'; import 'package:test/test.dart'; diff --git a/packages/ack_firebase_ai/test/live_firebase_ai_response_json_schema_test.dart b/packages/ack_firebase_ai/test/live_firebase_ai_response_json_schema_test.dart index f4b761c3..6a270a83 100644 --- a/packages/ack_firebase_ai/test/live_firebase_ai_response_json_schema_test.dart +++ b/packages/ack_firebase_ai/test/live_firebase_ai_response_json_schema_test.dart @@ -64,20 +64,16 @@ void main() { }); for (final schemaCase in _liveFirebaseAiSchemaCases()) { - test( - 'generates valid JSON for ${schemaCase.name}', - () async { - final generatedJson = await _generateLiveFirebaseJson( - app: app, - config: liveConfig!, - schemaCase: schemaCase, - ); - - schemaCase.expectGeneratedJson(generatedJson); - _expectAckValid(schemaCase.schema, generatedJson); - }, - timeout: const Timeout(Duration(minutes: 2)), - ); + test('generates valid JSON for ${schemaCase.name}', () async { + final generatedJson = await _generateLiveFirebaseJson( + app: app, + config: liveConfig!, + schemaCase: schemaCase, + ); + + schemaCase.expectGeneratedJson(generatedJson); + _expectAckValid(schemaCase.schema, generatedJson); + }, timeout: const Timeout(Duration(minutes: 2))); } }, ); diff --git a/packages/ack_generator/CHANGELOG.md b/packages/ack_generator/CHANGELOG.md index 627ecf38..26500083 100644 --- a/packages/ack_generator/CHANGELOG.md +++ b/packages/ack_generator/CHANGELOG.md @@ -1,3 +1,18 @@ +## Unreleased + +* Read annotations from `package:ack` while preserving compatibility imports + through `ack_annotations`. +* Make `@Schemable()` the canonical annotation for schema-first and class-first + generation. `@AckInfer()` and `@AckModel()` remain deprecated spellings. + Class-first models use concrete constructor-backed fields, including Dart + primary constructors. +* Upgrade the generator to Analyzer 13 and support its unified argument and + formal-parameter AST APIs. +* Require Dart 3.13 for `ack_generator`. The core `ack` package keeps its + Dart 3.9 minimum. +* Emit the non-deprecated `Schemable.generatedJson` marker and preserve + required capture-map types in generated JSON helpers. + ## 1.7.0-beta.2 ### Breaking diff --git a/packages/ack_generator/README.md b/packages/ack_generator/README.md index 2d75c380..c58f4b63 100644 --- a/packages/ack_generator/README.md +++ b/packages/ack_generator/README.md @@ -1,20 +1,23 @@ # Ack Generator -`ack_generator` supports two modern directions: `@AckInfer()` turns a -top-level Ack schema into an immutable model, while `@AckModel()` derives an -Ack codec schema from a hand-written class. It also retains the deprecated Ack -1.1 `@AckType()` generator unchanged. +Requires Dart 3.13 or newer. The generator uses Analyzer 13.3 and dart_style +3.1.13. The core `ack` runtime still supports Dart 3.9. + +`ack_generator` supports two modern directions through `@Schemable()`: +it turns a top-level Ack schema into an immutable model and derives an Ack +codec schema from a hand-written class. `@AckInfer()` and `@AckModel()` are +deprecated spellings. It also retains the deprecated Ack 1.1 `@AckType()` +generator unchanged. ## Schema-first usage ```dart import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'user_schema.ack.dart'; part 'user_schema.ack.g.dart'; -@AckInfer() +@Schemable() final userSchema = Ack.object({ 'name': Ack.string(), 'email': Ack.string().email(), @@ -54,7 +57,7 @@ it. Generated models don't implement `Map`, and there are no `fromMap` or `toMap` aliases. Omit `name` when the inferred class name is right. Use -`@AckInfer(name: 'Member')` only when you need an exact custom name. Custom +`@Schemable(name: 'Member')` only when you need an exact custom name. Custom names must be unchanged UpperCamelCase identifiers. ## Class-first usage @@ -64,20 +67,18 @@ mixin: ```dart import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'account.ack.dart'; part 'account.ack.g.dart'; -@AckModel() -final class Account with _$AccountAck { - const Account({required this.name}); - - @MinLength(2) - final String name; - +@Schemable() +final class Account({required final String name}) with _$AccountAck { static final fromJson = AccountSchema.fromJson; } + +// Primary constructor fields are inferred automatically. +// A field whose type is another @Schemable class uses that nested schema. + ``` After generation, the public facade and model JSON methods use the same Ack @@ -91,7 +92,7 @@ void main() { } ``` -Use `@AckModel(description: ...)` and `@AckField(description: ...)` for schema +Use `@Schemable(description: ...)` and `@AckField(description: ...)` for schema descriptions. A single-line `@description` tag in a `///` or `/** */` doc comment is a fallback. Only the text after the tag, on the same line, is exported. Untagged prose never becomes schema data. Duplicate or blank tags fail generation. @@ -111,8 +112,8 @@ The generator supports objects, empty objects, scalar and collection roots, literals, enums, defaults, additional properties, built-in and custom bidirectional codecs, named nested models, aliases, named `Ack.lazy` recursion, and same-library discriminated unions. Lists, sets, and maps stored by a model -generated with `@AckInfer()` are copied recursively into unmodifiable -collections. `@AckModel()` parsing provides the same guarantee, including for +generated with `@Schemable()` are copied recursively into unmodifiable +collections. Class-first parsing provides the same guarantee, including for captured extras. Hand-written constructors and collection replacements passed to `copyWith` remain responsible for their own defensive copies; use `deepUnmodifiableJsonMap` for dynamic JSON maps. Raw @@ -180,7 +181,7 @@ targets: `@AckInfer()` can annotate top-level schema variables and top-level schema getters. Classes, instance members, and local variables are rejected. -`@AckModel()` annotates public, constructable `final class` declarations whose +`@Schemable()` annotates public, constructable `final class` declarations whose stored fields are final. Annotated sealed union bases remain supported, and their concrete branches must also be final. Use `@Optional()` or `@Required()` to override inferred key presence, `@NotNull()` to reject JSON `null` without diff --git a/packages/ack_generator/example/example.dart b/packages/ack_generator/example/example.dart index 1c472593..f46e28e6 100644 --- a/packages/ack_generator/example/example.dart +++ b/packages/ack_generator/example/example.dart @@ -14,15 +14,14 @@ // dart run build_runner build --delete-conflicting-outputs // ``` // -// Add `ack` and `ack_annotations` to `dependencies`, and `ack_generator` plus -// `build_runner` to `dev_dependencies`. +// Add `ack` to `dependencies`, and `ack_generator` plus `build_runner` to +// `dev_dependencies`. import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; -/// Schema-first. `@AckInfer()` derives an immutable `User` class from this +/// Schema-first. `@Schemable()` derives an immutable `User` class from this /// schema: final `name`, `email`, and `age` fields, `User.parse`, /// `User.fromJson`, and `toJson`. -@AckInfer() +@Schemable() final userSchema = Ack.object({ 'name': Ack.string().minLength(2).maxLength(50), 'email': Ack.string().email(), @@ -31,16 +30,16 @@ final userSchema = Ack.object({ /// `name` overrides the class name that the declaration would imply. /// This schema generates `AppSettings` rather than `Settings`. -@AckInfer(name: 'AppSettings') +@Schemable(name: 'AppSettings') final settingsSchema = Ack.object({ 'theme': Ack.enumString(['light', 'dark']), 'notifications': Ack.boolean(), }); -/// Class-first. `@AckModel()` runs the other direction: it derives a validated +/// Class-first. `@Schemable()` runs the other direction: it derives a validated /// schema from this hand-written class, which applies the generated /// `_$ProductAck` mixin once the parts are in place. -@AckModel(caseStyle: AckCaseStyle.snake) +@Schemable(caseStyle: AckCaseStyle.snake) final class Product { const Product({required this.displayName, required this.priceCents}); diff --git a/packages/ack_generator/lib/ack_generator.dart b/packages/ack_generator/lib/ack_generator.dart index 065fb48c..ca06f5a7 100644 --- a/packages/ack_generator/lib/ack_generator.dart +++ b/packages/ack_generator/lib/ack_generator.dart @@ -3,4 +3,4 @@ export 'src/builder.dart' show ackGenerator, ackModelBuilder, ackModelJsonBuilder; // Re-export AckType for convenience alongside the builder entrypoint. -export 'package:ack_annotations/ack_annotations.dart'; +export 'package:ack/annotations.dart'; diff --git a/packages/ack_generator/lib/src/analyzer/class_model_graph_builder.dart b/packages/ack_generator/lib/src/analyzer/class_model_graph_builder.dart index 2b24254b..a1dbc46e 100644 --- a/packages/ack_generator/lib/src/analyzer/class_model_graph_builder.dart +++ b/packages/ack_generator/lib/src/analyzer/class_model_graph_builder.dart @@ -1,6 +1,8 @@ +// ignore_for_file: deprecated_member_use + import 'package:ack/ack.dart' show AckSchema; -import 'package:ack_annotations/ack_annotations.dart' as annotations; -import 'package:ack_annotations/ack_generator_support.dart'; +import 'package:ack/annotations.dart' as annotations; +import 'package:ack/ack_generator_support.dart'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; @@ -33,7 +35,7 @@ typedef _FutureGeneratedType = ({ String? setListSchema, }); -typedef _ClassFirstDependency = ({ClassElement target, FieldElement field}); +typedef _ClassFirstDependency = ({ClassElement target, VariableElement field}); /// Builds normalized Ack model nodes from hand-written `@AckModel` classes. /// @@ -143,20 +145,23 @@ final class ClassModelGraphBuilder { static const _ackModelChecker = TypeChecker.typeNamed( annotations.AckModel, - inPackage: 'ack_annotations', + inPackage: 'ack', + ); + static const _schemableChecker = TypeChecker.typeNamed( + annotations.Schemable, + inPackage: 'ack', ); static const _ackInferChecker = TypeChecker.typeNamed( annotations.AckInfer, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _ackTypeChecker = TypeChecker.typeNamed( - // ignore: deprecated_member_use annotations.AckType, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _ackFieldChecker = TypeChecker.typeNamed( annotations.AckField, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _ackSchemaChecker = TypeChecker.typeNamed( AckSchema, @@ -164,7 +169,7 @@ final class ClassModelGraphBuilder { ); static const _generatedJsonChecker = TypeChecker.typeNamed( AckGeneratedJson, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _jsonSerializableChecker = TypeChecker.typeNamed( JsonSerializable, @@ -177,15 +182,15 @@ final class ClassModelGraphBuilder { static const _optionalChecker = TypeChecker.typeNamed( annotations.Optional, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _requiredChecker = TypeChecker.typeNamed( annotations.Required, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _notNullChecker = TypeChecker.typeNamed( annotations.NotNull, - inPackage: 'ack_annotations', + inPackage: 'ack', ); final LibraryReader library; @@ -230,7 +235,8 @@ final class ClassModelGraphBuilder { element: element, ); } - if (element.isAbstract || !element.isConstructable) { + if ((element.isAbstract || !element.isConstructable) && + element.unnamedConstructor?.isFactory != true) { throw InvalidGenerationSource( '@AckModel requires a constructable class; ${element.name} is ' 'abstract.', @@ -456,12 +462,19 @@ final class ClassModelGraphBuilder { _graph.begin(id); _registerMetadata(element, options, id); final constructor = element.unnamedConstructor; - if (constructor == null || !constructor.isGenerative) { + if (constructor == null) { throw InvalidGenerationSource( '@AckModel ${element.name} requires an unnamed generative constructor.', element: element, ); } + if (constructor.isFactory) { + throw InvalidGenerationSource( + '@AckModel ${element.name} requires an unnamed generative constructor ' + 'with fields declared in the constructor.', + element: constructor, + ); + } _requireMixin(element); _rejectGeneratedMemberCollisions(element, includeValueMembers: true); @@ -531,6 +544,7 @@ final class ClassModelGraphBuilder { ? AckConstructorParameterKind.named : AckConstructorParameterKind.positional, fieldName: fieldName, + isRequired: parameter.isRequiredNamed, typeRef: futureTypes[fieldName]?.runtimeRef ?? _typeRef(parameter.type, field!), @@ -562,7 +576,7 @@ final class ClassModelGraphBuilder { } final nodes = []; - final ownerByJsonKey = {}; + final ownerByJsonKey = {}; for (final field in fields.values) { final name = field.name; if (name == null || name == captureFieldName) continue; @@ -675,7 +689,7 @@ final class ClassModelGraphBuilder { void _recordClassFirstDependencies( ClassElement owner, DartType type, - FieldElement field, + VariableElement field, ) { if (type is! InterfaceType) return; final target = type.element; @@ -699,7 +713,8 @@ final class ClassModelGraphBuilder { if (recorded != null) return recorded; _dependencies[owner] = <_ClassFirstDependency>[]; - for (final field in _instanceFields(owner).values) { + final Iterable fields = _instanceFields(owner).values; + for (final field in fields) { _recordClassFirstDependencies(owner, field.type, field); } return _dependencies[owner]!; @@ -712,7 +727,7 @@ final class ClassModelGraphBuilder { if (visiting.contains(dependency.target)) { final field = dependency.field; throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} creates a ' + '${field.enclosingElement?.name}.${field.name} creates a ' 'recursive class-first schema graph. Automatic class-first ' 'Ack.lazy semantics are not yet defined; use a schema-first ' 'named Ack.lazy contract for recursive models.', @@ -731,7 +746,7 @@ final class ClassModelGraphBuilder { } Future _fieldSchema( - FieldElement field, { + VariableElement field, { _FutureGeneratedType? futureType, }) async { final override = _ackFieldChecker.firstAnnotationOfExact(field); @@ -800,7 +815,10 @@ final class ClassModelGraphBuilder { type is InvalidType || (type is InterfaceType && type.typeArguments.any(_containsInvalidType)); - void _rejectResolvedLegacyGeneratedType(DartType type, FieldElement field) { + void _rejectResolvedLegacyGeneratedType( + DartType type, + VariableElement field, + ) { if (type is! InterfaceType) return; final element = type.element; if (element is ExtensionTypeElement) { @@ -824,20 +842,28 @@ final class ClassModelGraphBuilder { return false; } - Future<_FutureGeneratedType?> _futureGeneratedType(FieldElement field) async { - final resolved = await _resolvedLibraryFor(field.library); + Future<_FutureGeneratedType?> _futureGeneratedType( + VariableElement field, + ) async { + final resolved = await _resolvedLibraryFor(field.library!); AstNode? node = resolved.getFragmentDeclaration(field.firstFragment)?.node; - while (node != null && node is! FieldDeclaration) { + while (node != null && + node is! FieldDeclaration && + node is! FormalParameter) { node = node.parent; } - final annotation = node is FieldDeclaration ? node.fields.type : null; + final annotation = switch (node) { + FieldDeclaration() => node.fields.type, + FormalParameter() => node.type, + _ => null, + }; if (annotation == null) return null; return _futureGeneratedTypeForAnnotation(annotation, field); } _FutureGeneratedType? _futureGeneratedTypeForAnnotation( TypeAnnotation annotation, - FieldElement field, + VariableElement field, ) { if (annotation is! NamedType) return null; final name = annotation.name.lexeme; @@ -903,7 +929,7 @@ final class ClassModelGraphBuilder { Element? _futureAckInferTarget({ required String className, required String? prefix, - required FieldElement field, + required VariableElement field, }) { final matches = {}; final candidates = {}; @@ -960,7 +986,7 @@ final class ClassModelGraphBuilder { } if (matches.isEmpty) { if (hiddenDeclaration != null) { - final fieldPath = '${field.enclosingElement.name}.${field.name}'; + final fieldPath = '${field.enclosingElement?.name}.${field.name}'; throw InvalidGenerationSource( '$fieldPath resolves to generated schema-first type "$className", ' 'which is hidden by an import combinator or an upstream export ' @@ -972,7 +998,7 @@ final class ClassModelGraphBuilder { } if (matches.length > 1) { throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} resolves future generated ' + '${field.enclosingElement?.name}.${field.name} resolves future generated ' 'type "$className" ambiguously.', element: field, ); @@ -983,7 +1009,7 @@ final class ClassModelGraphBuilder { Element? _futureAckTypeTarget({ required String generatedTypeName, required String? prefix, - required FieldElement field, + required VariableElement field, }) { final matches = {}; String? hiddenDeclaration; @@ -1032,7 +1058,7 @@ final class ClassModelGraphBuilder { } if (matches.length > 1) { throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} resolves future legacy ' + '${field.enclosingElement?.name}.${field.name} resolves future legacy ' 'generated type "$generatedTypeName" ambiguously.', element: field, ); @@ -1040,9 +1066,9 @@ final class ClassModelGraphBuilder { return matches.single; } - Never _rejectLegacyGeneratedType(FieldElement field, String name) { + Never _rejectLegacyGeneratedType(VariableElement field, String name) { throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} crosses from modern ' + '${field.enclosingElement?.name}.${field.name} crosses from modern ' '@AckModel into legacy @AckType generated type "$name". AckType ' 'and modern models intentionally use isolated generators; migrate ' 'this connected graph together.', @@ -1084,13 +1110,16 @@ final class ClassModelGraphBuilder { _ => null, }; return declaration != null && - _ackInferChecker.hasAnnotationOfExact(declaration) + (_ackInferChecker.hasAnnotationOfExact(declaration) || + _schemableChecker.hasAnnotationOfExact(declaration)) ? declaration : null; } String _generatedAckInferClassName(Element declaration) { - final annotation = _ackInferChecker.firstAnnotationOfExact(declaration)!; + final annotation = + _ackInferChecker.firstAnnotationOfExact(declaration) ?? + _schemableChecker.firstAnnotationOfExact(declaration)!; final custom = ConstantReader(annotation).read('name'); return ackInferModelClassName( declaration.name!, @@ -1113,13 +1142,13 @@ final class ClassModelGraphBuilder { } Future _escapeHatchExpression( - FieldElement field, + VariableElement field, ConstantReader annotation, ) async { final function = annotation.read('schema').objectValue.toFunctionValue(); if (function is! TopLevelFunctionElement) { throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} @AckField schema must ' + '${field.enclosingElement?.name}.${field.name} @AckField schema must ' 'be a const tear-off of a top-level function.', element: field, ); @@ -1127,7 +1156,7 @@ final class ClassModelGraphBuilder { if (function.formalParameters.isNotEmpty || !_ackSchemaChecker.isAssignableFromType(function.returnType)) { throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} @AckField top-level ' + '${field.enclosingElement?.name}.${field.name} @AckField top-level ' 'function must have type AckSchema Function().', element: field, ); @@ -1141,7 +1170,7 @@ final class ClassModelGraphBuilder { : null; if (bodyExpression == null) { throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} @AckField function must ' + '${field.enclosingElement?.name}.${field.name} @AckField function must ' 'have a statically resolvable expression or single return.', element: field, ); @@ -1149,7 +1178,7 @@ final class ClassModelGraphBuilder { final oneWaySource = await _oneWayTransformSource(bodyExpression); if (oneWaySource != null) { throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} @AckField schema ' + '${field.enclosingElement?.name}.${field.name} @AckField schema ' 'function ${function.name} reaches one-way schema $oneWaySource. ' 'Migrate this .transform() path to .codec() with an encoder.', element: field, @@ -1159,7 +1188,7 @@ final class ClassModelGraphBuilder { return '${prefix == null ? '' : '$prefix.'}${function.name}()'; } - Future _schemaForType(DartType type, FieldElement field) => + Future _schemaForType(DartType type, VariableElement field) => _inference.inferType( type, visibleTypeName: _visibleTypeName, @@ -1173,7 +1202,7 @@ final class ClassModelGraphBuilder { Future _namedSchemaForType( InterfaceType interfaceType, - FieldElement field, + VariableElement field, ) async { final target = interfaceType.element; if (target is ClassElement) { @@ -1195,13 +1224,16 @@ final class ClassModelGraphBuilder { return null; } - void _rejectNullableCollectionElement(FieldElement field, DartType itemType) { + void _rejectNullableCollectionElement( + VariableElement field, + DartType itemType, + ) { if (!_isNullable(itemType)) return; _nullableCollectionElementError(field, itemType.getDisplayString()); } void _rejectNullableFutureCollectionElement( - FieldElement field, + VariableElement field, AckInferRef type, ) { switch (type) { @@ -1220,9 +1252,12 @@ final class ClassModelGraphBuilder { } } - Never _nullableCollectionElementError(FieldElement field, String itemType) { + Never _nullableCollectionElementError( + VariableElement field, + String itemType, + ) { throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} uses nullable collection ' + '${field.enclosingElement?.name}.${field.name} uses nullable collection ' 'elements ($itemType). Ack.list does not support ' 'nullable item schemas; make the element non-nullable or provide an ' 'explicit @AckField(schema: ...) codec.', @@ -1234,7 +1269,7 @@ final class ClassModelGraphBuilder { ClassElement target, String facadeName, { required String? prefix, - required FieldElement field, + required VariableElement field, }) { if (target.library == library.element) return; final modelName = target.name!; @@ -1270,27 +1305,27 @@ final class ClassModelGraphBuilder { } } - String _applySugar(String schema, FieldElement field) => + String _applySugar(String schema, VariableElement field) => _inference.applyConstraints(schema, field, field.type); - void _rejectUnsupportedStaticType(FieldElement field, DartType type) { + void _rejectUnsupportedStaticType(VariableElement field, DartType type) { if (type is DynamicType || type is TypeParameterType) { _unsupportedFieldType(field, type); } } - Never _unsupportedFieldType(FieldElement field, DartType type) { + Never _unsupportedFieldType(VariableElement field, DartType type) { final jsonHint = type is DynamicType ? ', or Object? for an open JSON value' : ''; throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} uses unsupported ' + '${field.enclosingElement?.name}.${field.name} uses unsupported ' '${type.getDisplayString()}; use a concrete type with a static ' 'class-first schema contract$jsonHint.', element: field, ); } - void _validateMapKey(FieldElement field, DartType type) { + void _validateMapKey(VariableElement field, DartType type) { if (type is! InterfaceType || !type.isDartCoreMap || type.typeArguments.length != 2) { @@ -1299,13 +1334,13 @@ final class ClassModelGraphBuilder { final key = type.typeArguments.first; if (key is InterfaceType && _isCore(key, 'String')) return; throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} must use Map; ' + '${field.enclosingElement?.name}.${field.name} must use Map; ' 'received ${type.getDisplayString()}.', element: field, ); } - AckInferRef _typeRef(DartType type, FieldElement field) { + AckInferRef _typeRef(DartType type, VariableElement field) { if (type is DynamicType || type is TypeParameterType) { _unsupportedFieldType(field, type); } @@ -1414,7 +1449,7 @@ final class ClassModelGraphBuilder { } AckSchemaFieldPresence _effectivePresence( - FieldElement field, { + VariableElement field, { required FormalParameterElement? parameter, required bool isDiscriminator, }) { @@ -1423,7 +1458,7 @@ final class ClassModelGraphBuilder { final hasRequired = _requiredChecker.hasAnnotationOfExact(field); if (hasOptional && hasRequired) { throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} cannot combine ' + '${field.enclosingElement?.name}.${field.name} cannot combine ' '@Optional() and @Required().', element: field, ); @@ -1444,7 +1479,7 @@ final class ClassModelGraphBuilder { reader.read('description').isNull && presenceIndex == 0) { throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} @AckField() is a ' + '${field.enclosingElement?.name}.${field.name} @AckField() is a ' 'no-op; set schema, description, or presence.', element: field, ); @@ -1470,7 +1505,7 @@ final class ClassModelGraphBuilder { annotationOverride != null && legacyOverride != annotationOverride) { throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} has conflicting ' + '${field.enclosingElement?.name}.${field.name} has conflicting ' 'presence declarations.', element: field, ); @@ -1484,7 +1519,7 @@ final class ClassModelGraphBuilder { (!parameter.isRequired || _isNullable(parameter.type))); if (!canBeOptional) { throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} cannot be optional ' + '${field.enclosingElement?.name}.${field.name} cannot be optional ' 'because the constructor cannot accept a missing value.', element: field, ); @@ -1493,7 +1528,7 @@ final class ClassModelGraphBuilder { if (legacyOverride != null) { log.warning( - '${field.enclosingElement.name}.${field.name} uses ' + '${field.enclosingElement?.name}.${field.name} uses ' '@AckField(presence: ...); use @Optional() or @Required() instead. ' 'AckField.presence will be removed in 2.0.0.', ); @@ -1740,7 +1775,9 @@ final class ClassModelGraphBuilder { } _ModelOptions? _options(ClassElement element) { - final annotation = _ackModelChecker.firstAnnotationOfExact(element); + final annotation = + _ackModelChecker.firstAnnotationOfExact(element) ?? + _schemableChecker.firstAnnotationOfExact(element); if (annotation == null) return null; final reader = ConstantReader(annotation); final caseStyle = reader.read('caseStyle').objectValue; @@ -1801,14 +1838,15 @@ final class ClassModelGraphBuilder { return base is ClassElement && base.library == element.library && base.isSealed && - _ackModelChecker.hasAnnotationOfExact(base); + (_ackModelChecker.hasAnnotationOfExact(base) || + _schemableChecker.hasAnnotationOfExact(base)); }); return isImplicitUnionBranch ? ackClassSchemaFacadeName(element.name!) : null; } - String? _jsonKey(FieldElement field) { + String? _jsonKey(VariableElement field) { final annotation = _jsonKeyChecker.firstAnnotationOfExact(field); if (annotation == null) return null; final reader = ConstantReader(annotation); @@ -1832,7 +1870,7 @@ final class ClassModelGraphBuilder { ]; if (configuredUnsupported.isNotEmpty) { throw InvalidGenerationSource( - '${field.enclosingElement.name}.${field.name} uses unsupported ' + '${field.enclosingElement?.name}.${field.name} uses unsupported ' '@JsonKey options: ${configuredUnsupported.join(', ')}. @AckModel ' 'supports only @JsonKey(name: ...) on fields so validation and JSON ' 'serialization cannot diverge.', diff --git a/packages/ack_generator/lib/src/analyzer/schema_ast_analyzer.dart b/packages/ack_generator/lib/src/analyzer/schema_ast_analyzer.dart index 5fa44826..8674d268 100644 --- a/packages/ack_generator/lib/src/analyzer/schema_ast_analyzer.dart +++ b/packages/ack_generator/lib/src/analyzer/schema_ast_analyzer.dart @@ -1,5 +1,7 @@ +// ignore_for_file: deprecated_member_use + import 'package:collection/collection.dart'; -import 'package:ack_annotations/ack_annotations.dart'; +import 'package:ack/annotations.dart'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/token.dart' show Keyword; @@ -75,13 +77,12 @@ class _ResolvedSchemaReference { /// requiring const evaluation or string parsing. class SchemaAstAnalyzer { static const _ackTypeChecker = TypeChecker.typeNamed( - // ignore: deprecated_member_use AckType, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _ackModelChecker = TypeChecker.typeNamed( AckModel, - inPackage: 'ack_annotations', + inPackage: 'ack', ); final Map _schemaVariableTypeCache = {}; @@ -261,8 +262,7 @@ class SchemaAstAnalyzer { throw InvalidGenerationSource( 'Schema getter "${element.name}" must return a schema expression', element: element, - todo: - 'Use an expression body or a single return statement (e.g., return Ack.object({...});).', + todo: 'Use an expression body or a single return statement (e.g., return Ack.object({...});).', ); } @@ -303,8 +303,7 @@ class SchemaAstAnalyzer { throw InvalidGenerationSource( 'Schema getter "${element.name}" must return an Ack schema invocation or schema reference', element: element, - todo: - 'Return a schema expression such as Ack.object({...}), Ack.string(), or another @AckType schema variable/getter.', + todo: 'Return a schema expression such as Ack.object({...}), Ack.string(), or another @AckType schema variable/getter.', ); } @@ -677,11 +676,11 @@ class SchemaAstAnalyzer { SetOrMapLiteral? schemasLiteral; for (final argument in baseInvocation.argumentList.arguments) { - if (argument is! NamedExpression) continue; + if (argument is! NamedArgument) continue; - final name = argument.name.label.name; + final name = argument.name.lexeme; if (name == 'discriminatorKey') { - final expression = argument.expression; + final expression = argument.argumentExpression; if (expression is! SimpleStringLiteral) { throw InvalidGenerationSource( 'Ack.discriminated(...): `discriminatorKey` must be a string literal.', @@ -690,7 +689,7 @@ class SchemaAstAnalyzer { } discriminatorKey = expression.value; } else if (name == 'schemas') { - final expression = argument.expression; + final expression = argument.argumentExpression; if (expression is! SetOrMapLiteral) { throw InvalidGenerationSource( 'Ack.discriminated(...): `schemas` must be a map literal.', @@ -771,8 +770,7 @@ class SchemaAstAnalyzer { throw InvalidGenerationSource( 'Ack.discriminated(...): branch "$discriminatorValue" must reference a top-level schema variable/getter.', element: element, - todo: - 'Extract inline expressions to a top-level @AckType schema variable/getter and reference it.', + todo: 'Extract inline expressions to a top-level @AckType schema variable/getter and reference it.', ); } @@ -817,8 +815,7 @@ class SchemaAstAnalyzer { 'Ack.discriminated(...): branch "${resolvedBranch.schemaName}" is itself a discriminated base. ' 'Nested discriminated unions are not supported.', element: element, - todo: - 'Use a plain Ack.object(...) schema for each branch, not another Ack.discriminated(...).', + todo: 'Use a plain Ack.object(...) schema for each branch, not another Ack.discriminated(...).', ); } @@ -1238,10 +1235,10 @@ class SchemaAstAnalyzer { // First check for named parameter in the base Ack.object() call for (final arg in baseInvocation.argumentList.arguments) { - if (arg is NamedExpression && - arg.name.label.name == 'additionalProperties') { - if (arg.expression is BooleanLiteral) { - hasAdditionalProperties = (arg.expression as BooleanLiteral).value; + if (arg is NamedArgument && arg.name.lexeme == 'additionalProperties') { + if (arg.argumentExpression is BooleanLiteral) { + hasAdditionalProperties = + (arg.argumentExpression as BooleanLiteral).value; } } } @@ -1495,8 +1492,7 @@ class SchemaAstAnalyzer { 'Field "$fieldName" schema method chain exceeded max depth of 20. ' '@AckType requires statically analyzable schema chains.', element: element, - todo: - 'Reduce the chaining depth or extract part of the schema into a named variable.', + todo: 'Reduce the chaining depth or extract part of the schema into a named variable.', ); } @@ -1524,8 +1520,7 @@ class SchemaAstAnalyzer { 'Field "$fieldName" uses anonymous inline Ack.object(...). ' 'Strict typed generation requires a named schema reference.', element: element, - todo: - 'Extract this inline object schema into a top-level @AckType() variable and reference it by name.', + todo: 'Extract this inline object schema into a top-level @AckType() variable and reference it by name.', ); } @@ -1836,7 +1831,7 @@ class SchemaAstAnalyzer { final args = listInvocation.argumentList.arguments; if (args.isEmpty) return null; - final ref = _resolveListElementRef(args.first); + final ref = _resolveListElementRef(args.first.argumentExpression); final elementSchema = ref.invocation == null ? null : _analyzeSchemaChain(ref.invocation!).ackBase; @@ -1879,7 +1874,7 @@ class SchemaAstAnalyzer { final args = invocation.argumentList.arguments; if (args.isNotEmpty) { final resolvedFromArgument = _resolveEnumValuesTypeFromArgument( - args.first, + args.first.argumentExpression, library: library, ); if (resolvedFromArgument != null) { @@ -2120,19 +2115,18 @@ class SchemaAstAnalyzer { throw InvalidGenerationSource( 'Ack.list(...) requires an element schema argument for strict typed generation.', element: element, - todo: - 'Provide a concrete element schema, e.g. Ack.list(Ack.string()) or Ack.list(namedSchema).', + todo: 'Provide a concrete element schema, e.g. Ack.list(Ack.string()) or Ack.list(namedSchema).', ); } final firstArg = args.first; _rejectAckModelFacadeExpression( - firstArg, + firstArg.argumentExpression, element, diagnosticPath: diagnosticPath, ); - final ref = _resolveListElementRef(firstArg); + final ref = _resolveListElementRef(firstArg.argumentExpression); if (ref.invocation != null) { final chain = _analyzeSchemaChain(ref.invocation!); _rejectNullableListElement(chain.isNullable, element); @@ -2149,8 +2143,7 @@ class SchemaAstAnalyzer { 'Ack.list(Ack.object(...)) uses an anonymous inline object schema. ' 'Strict typed generation requires a named schema reference.', element: element, - todo: - 'Extract the inline object to a top-level @AckType() variable and use Ack.list(namedSchema).', + todo: 'Extract the inline object to a top-level @AckType() variable and use Ack.list(namedSchema).', ); } @@ -2178,8 +2171,7 @@ class SchemaAstAnalyzer { throw InvalidGenerationSource( 'Could not statically resolve Ack.list($rawExpression) element type.', element: element, - todo: - 'Use Ack.list(Ack.()), Ack.list(enumSchema), or Ack.list(namedSchema) so the generator can infer a concrete element type.', + todo: 'Use Ack.list(Ack.()), Ack.list(enumSchema), or Ack.list(namedSchema) so the generator can infer a concrete element type.', ); } @@ -2238,8 +2230,7 @@ class SchemaAstAnalyzer { throw InvalidGenerationSource( 'Could not statically resolve Ack.list($rawExpression) element type.', element: element, - todo: - 'Use Ack.list(Ack.()), Ack.list(enumSchema), or Ack.list(namedSchema) so the generator can infer a concrete element type.', + todo: 'Use Ack.list(Ack.()), Ack.list(enumSchema), or Ack.list(namedSchema) so the generator can infer a concrete element type.', ); } @@ -2249,8 +2240,7 @@ class SchemaAstAnalyzer { throw InvalidGenerationSource( 'Ack.list(...) does not support nullable element schemas.', element: element, - todo: - 'Remove `.nullable()` from the element schema. Make the list itself nullable with `Ack.list(item).nullable()` when needed.', + todo: 'Remove `.nullable()` from the element schema. Make the list itself nullable with `Ack.list(item).nullable()` when needed.', ); } @@ -2474,8 +2464,7 @@ class SchemaAstAnalyzer { throw InvalidGenerationSource( 'Circular schema reference detected for "$referenceLabel".', element: contextElement, - todo: - 'Break the circular alias/reference chain between @AckType schemas.', + todo: 'Break the circular alias/reference chain between @AckType schemas.', ); } @@ -2829,7 +2818,7 @@ class SchemaAstAnalyzer { bool _hasAckInferAnnotation(Element element) { return TypeChecker.typeNamed( AckInfer, - inPackage: 'ack_annotations', + inPackage: 'ack', ).hasAnnotationOfExact(element); } @@ -2874,8 +2863,7 @@ class SchemaAstAnalyzer { '"${resolved.schemaName}" uses unsupported syntax for cross-file ' 'generation.', element: contextElement, - todo: - 'Use a nominal type with optional nested generics/nullability, or keep the schema in the same library.', + todo: 'Use a nominal type with optional nested generics/nullability, or keep the schema in the same library.', ); } @@ -2919,8 +2907,7 @@ class SchemaAstAnalyzer { '"${resolved.schemaName}" uses a qualified type that cannot be ' 'referenced across library boundaries.', element: contextElement, - todo: - 'Use an unqualified exported representation type, import that type directly into the consuming library, or keep the schema in the same library.', + todo: 'Use an unqualified exported representation type, import that type directly into the consuming library, or keep the schema in the same library.', ); } @@ -2965,8 +2952,7 @@ class SchemaAstAnalyzer { 'Transformed representation type "$fullRepresentationType" for ' '"${resolved.schemaName}" is ambiguous in this library.', element: contextElement, - todo: - 'Use a prefixed schema import or rename/import the representation type so the generated cast resolves unambiguously.', + todo: 'Use a prefixed schema import or rename/import the representation type so the generated cast resolves unambiguously.', ); } } @@ -2976,8 +2962,7 @@ class SchemaAstAnalyzer { 'Transformed representation type "$fullRepresentationType" for ' '"${resolved.schemaName}" is ambiguous in this library.', element: contextElement, - todo: - 'Use a prefixed schema import or rename/import the representation type so the generated cast resolves unambiguously.', + todo: 'Use a prefixed schema import or rename/import the representation type so the generated cast resolves unambiguously.', ); } @@ -2989,8 +2974,7 @@ class SchemaAstAnalyzer { 'Transformed representation type "$fullRepresentationType" for ' '"${resolved.schemaName}" is not visible from this library.', element: contextElement, - todo: - 'Export the representation type from the referenced schema library or import that type directly into this library.', + todo: 'Export the representation type from the referenced schema library or import that type directly into this library.', ); } @@ -3285,8 +3269,7 @@ class SchemaAstAnalyzer { '$contextLabel uses .transform(...) without an explicit output type. ' '@AckType requires .transform(...) so the generated type can be inferred.', element: element, - todo: - 'Add an explicit type argument, for example .transform((value) => ...).', + todo: 'Add an explicit type argument, for example .transform((value) => ...).', ); } @@ -3305,8 +3288,7 @@ class SchemaAstAnalyzer { '$contextLabel transforms an Ack.object(...) schema. ' 'Transformed object schemas are not supported by @AckType.', element: element, - todo: - 'Remove .transform() from the object schema or expose the transformed result through a separate non-object schema.', + todo: 'Remove .transform() from the object schema or expose the transformed result through a separate non-object schema.', ); } @@ -3315,8 +3297,7 @@ class SchemaAstAnalyzer { '$contextLabel transforms an Ack.discriminated(...) schema. ' 'Transformed discriminated schemas are not supported by @AckType.', element: element, - todo: - 'Remove .transform() from the discriminated schema or expose the transformed result through a separate non-object schema.', + todo: 'Remove .transform() from the discriminated schema or expose the transformed result through a separate non-object schema.', ); } } @@ -3332,8 +3313,7 @@ class SchemaAstAnalyzer { '$contextLabel transforms referenced discriminated schema ' '"${resolved.schemaName}". Transformed discriminated schemas are not supported by @AckType.', element: element, - todo: - 'Remove .transform() from the referenced discriminated schema or expose a separate non-object schema.', + todo: 'Remove .transform() from the referenced discriminated schema or expose a separate non-object schema.', ); } @@ -3342,8 +3322,7 @@ class SchemaAstAnalyzer { '$contextLabel transforms referenced object schema ' '"${resolved.schemaName}". Transformed object schemas are not supported by @AckType.', element: element, - todo: - 'Remove .transform() from the referenced object schema or expose a separate non-object schema.', + todo: 'Remove .transform() from the referenced object schema or expose a separate non-object schema.', ); } } diff --git a/packages/ack_generator/lib/src/analyzer/schema_model_graph_builder.dart b/packages/ack_generator/lib/src/analyzer/schema_model_graph_builder.dart index f179107e..ee0897d0 100644 --- a/packages/ack_generator/lib/src/analyzer/schema_model_graph_builder.dart +++ b/packages/ack_generator/lib/src/analyzer/schema_model_graph_builder.dart @@ -1,7 +1,8 @@ +// ignore_for_file: deprecated_member_use + import 'package:ack/ack.dart' show AckSchema, AnyOfSchema, AnySchema, InstanceSchema, MapSchema; -import 'package:ack_annotations/ack_annotations.dart' - hide AckUnknownPropertyPolicy; +import 'package:ack/annotations.dart' hide AckUnknownPropertyPolicy; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; @@ -149,16 +150,23 @@ final class SchemaModelGraphBuilder { static const _ackInferChecker = TypeChecker.typeNamed( AckInfer, - inPackage: 'ack_annotations', + inPackage: 'ack', + ); + static const _schemableChecker = TypeChecker.typeNamed( + Schemable, + inPackage: 'ack', ); static const _legacyAckTypeChecker = TypeChecker.typeNamed( - // ignore: deprecated_member_use AckType, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _ackModelChecker = TypeChecker.typeNamed( AckModel, - inPackage: 'ack_annotations', + inPackage: 'ack', + ); + static const _schemableModelChecker = TypeChecker.typeNamed( + Schemable, + inPackage: 'ack', ); static const _ackSchemaChecker = TypeChecker.typeNamed( AckSchema, @@ -1135,7 +1143,9 @@ final class SchemaModelGraphBuilder { } String? _classFirstFacadeName(ClassElement element) { - final annotation = _ackModelChecker.firstAnnotationOfExact(element); + final annotation = + _ackModelChecker.firstAnnotationOfExact(element) ?? + _schemableModelChecker.firstAnnotationOfExact(element); if (annotation != null) { final value = ConstantReader(annotation).read('schemaName'); return ackClassSchemaFacadeName( @@ -1148,7 +1158,8 @@ final class SchemaModelGraphBuilder { return base is ClassElement && base.library == element.library && base.isSealed && - _ackModelChecker.hasAnnotationOfExact(base); + (_ackModelChecker.hasAnnotationOfExact(base) || + _schemableModelChecker.hasAnnotationOfExact(base)); }); return isImplicitUnionBranch ? ackClassSchemaFacadeName(element.name!) @@ -1377,7 +1388,9 @@ final class SchemaModelGraphBuilder { } bool _hasAckInfer(Element element) { - return _ackInferChecker.hasAnnotationOfExact(_propertyDeclaration(element)); + final declaration = _propertyDeclaration(element); + return _ackInferChecker.hasAnnotationOfExact(declaration) || + _schemableChecker.hasAnnotationOfExact(declaration); } bool _hasLegacyAckType(Element element) { @@ -1387,9 +1400,10 @@ final class SchemaModelGraphBuilder { } String? _annotationName(Element element) { - final annotation = _ackInferChecker.firstAnnotationOfExact( - _propertyDeclaration(element), - ); + final declaration = _propertyDeclaration(element); + final annotation = + _ackInferChecker.firstAnnotationOfExact(declaration) ?? + _schemableChecker.firstAnnotationOfExact(declaration); final field = annotation == null ? null : ConstantReader(annotation).peek('name'); @@ -1792,18 +1806,14 @@ final class SchemaModelGraphBuilder { return null; } - /// Normalizes Analyzer 10 argument nodes into the expression API used by the - /// graph. + /// Reads the value expressions from Analyzer argument nodes. List _argumentExpressions(ArgumentList argumentList) => argumentList.arguments - .map((argument) => _argumentExpression(argument)) + .map((argument) => argument.argumentExpression) .toList(growable: false); - Expression _argumentExpression(Expression argument) => - argument is NamedExpression ? argument.expression : argument; - - ({String name, Expression expression})? _namedArgument(Expression argument) => - argument is NamedExpression - ? (name: argument.name.label.name, expression: argument.expression) + ({String name, Expression expression})? _namedArgument(Argument argument) => + argument is NamedArgument + ? (name: argument.name.lexeme, expression: argument.argumentExpression) : null; } diff --git a/packages/ack_generator/lib/src/builders/class_model_emitter.dart b/packages/ack_generator/lib/src/builders/class_model_emitter.dart index f863af32..1519e56c 100644 --- a/packages/ack_generator/lib/src/builders/class_model_emitter.dart +++ b/packages/ack_generator/lib/src/builders/class_model_emitter.dart @@ -380,10 +380,12 @@ Map $function(${node.className} model) { if (node.captureFieldName case final capture?) { final fromName = ackClassFromRuntimeBridgeName(node.className, capture); final toName = ackClassToRuntimeBridgeName(node.className, capture); + final hasDefault = parametersByField[capture]?.defaultExpression != null; output ..writeln( - 'Map? $fromName(Object? value) => ' - 'value == null ? null : ' + 'Map${hasDefault ? '?' : ''} ' + '$fromName(Object? value) => ' + '${hasDefault ? 'value == null ? null : ' : ''}' '${_ack('deepUnmodifiableJsonMap')}(' 'value as Map);', ) diff --git a/packages/ack_generator/lib/src/builders/data_class_emitter.dart b/packages/ack_generator/lib/src/builders/data_class_emitter.dart index dbc0c2a1..74f0ac70 100644 --- a/packages/ack_generator/lib/src/builders/data_class_emitter.dart +++ b/packages/ack_generator/lib/src/builders/data_class_emitter.dart @@ -71,6 +71,8 @@ final class AckDataClassEmitter { ), ]; final members = [ + for (final field in stored) + '${_type(field.runtimeRef)} get ${field.dartName};', if (includeValueMembers) ...[ copyWithGetter( className: className, diff --git a/packages/ack_generator/lib/src/builders/model_emitter.dart b/packages/ack_generator/lib/src/builders/model_emitter.dart index deb7d2df..3393f339 100644 --- a/packages/ack_generator/lib/src/builders/model_emitter.dart +++ b/packages/ack_generator/lib/src/builders/model_emitter.dart @@ -9,14 +9,14 @@ import '../utils/string_literal.dart'; final class AckModelEmitter { AckModelEmitter({ this.ackPrefix, - this.ackInferPrefix, + this.schemablePrefix, this.schemaPrefixInScope = false, }); static const _schemaShorthandName = 'schema'; final String? ackPrefix; - final String? ackInferPrefix; + final String? schemablePrefix; /// Whether the annotated library imports a prefix named `schema`. final bool schemaPrefixInScope; @@ -1076,11 +1076,11 @@ return $helper({ ]; Expression _jsonMarker() { - final prefix = ackInferPrefix; + final prefix = schemablePrefix; final typeName = prefix == null || prefix.isEmpty - ? 'AckInfer' - : '$prefix.AckInfer'; - return refer(typeName).property('jsonSerializable'); + ? 'Schemable' + : '$prefix.Schemable'; + return refer(typeName).property('generatedJson'); } String _ack(String symbol) { diff --git a/packages/ack_generator/lib/src/generator.dart b/packages/ack_generator/lib/src/generator.dart index 7b28b6ae..1bfc11cb 100644 --- a/packages/ack_generator/lib/src/generator.dart +++ b/packages/ack_generator/lib/src/generator.dart @@ -1,4 +1,4 @@ -import 'package:ack_annotations/ack_annotations.dart'; +import 'package:ack/annotations.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:build/build.dart'; import 'package:code_builder/code_builder.dart'; @@ -19,7 +19,7 @@ class AckSchemaGenerator extends Generator { static const _ackTypeChecker = TypeChecker.typeNamed( // ignore: deprecated_member_use AckType, - inPackage: 'ack_annotations', + inPackage: 'ack', ); final _formatter = DartFormatter( @@ -36,8 +36,7 @@ class AckSchemaGenerator extends Generator { throw InvalidGenerationSource( '@AckType can only be applied to top-level schema variables or getters, not classes.', element: element, - todo: - 'Remove @AckType from the class and annotate a top-level schema variable or getter instead.', + todo: 'Remove @AckType from the class and annotate a top-level schema variable or getter instead.', ); } @@ -50,8 +49,7 @@ class AckSchemaGenerator extends Generator { throw InvalidGenerationSource( '@AckType can only be applied to top-level schema variables or getters.', element: element, - todo: - 'Move this getter to the library level or annotate a top-level schema variable instead.', + todo: 'Move this getter to the library level or annotate a top-level schema variable instead.', ); } @@ -67,8 +65,7 @@ class AckSchemaGenerator extends Generator { throw InvalidGenerationSource( '@AckType can only be applied to top-level schema variables or getters.', element: getter, - todo: - 'Move this getter to the library level or annotate a top-level schema variable instead.', + todo: 'Move this getter to the library level or annotate a top-level schema variable instead.', ); } } @@ -99,8 +96,7 @@ class AckSchemaGenerator extends Generator { throw InvalidGenerationSource( 'Failed to analyze schema variable "${variable.name}": $e', element: variable, - todo: - 'Ensure the variable uses Ack schema syntax such as Ack.object(), Ack.string(), or another @AckType schema reference.', + todo: 'Ensure the variable uses Ack schema syntax such as Ack.object(), Ack.string(), or another @AckType schema reference.', ); } } @@ -118,8 +114,7 @@ class AckSchemaGenerator extends Generator { throw InvalidGenerationSource( 'Failed to analyze schema getter "${getter.name}": $e', element: getter, - todo: - 'Ensure the getter returns Ack schema syntax such as Ack.object(), Ack.string(), or another @AckType schema reference.', + todo: 'Ensure the getter returns Ack schema syntax such as Ack.object(), Ack.string(), or another @AckType schema reference.', ); } } @@ -191,8 +186,7 @@ class AckSchemaGenerator extends Generator { ) ?? (throw InvalidGenerationSource( 'Could not find schema declaration "${model.schemaClassName}"', - todo: - 'Ensure the schema variable or getter exists and is annotated with @AckType.', + todo: 'Ensure the schema variable or getter exists and is annotated with @AckType.', )), ]; @@ -204,8 +198,7 @@ class AckSchemaGenerator extends Generator { throw InvalidGenerationSource( 'Extension type dependency resolution failed: $e', element: element, - todo: - 'Check for circular dependencies in the typed schema graph and ensure nested schemas resolve to @AckType declarations.', + todo: 'Check for circular dependencies in the typed schema graph and ensure nested schemas resolve to @AckType declarations.', ); } @@ -220,8 +213,7 @@ class AckSchemaGenerator extends Generator { if (element == null) { throw InvalidGenerationSource( 'Could not find schema declaration "${model.schemaClassName}"', - todo: - 'Ensure the schema variable or getter exists and is annotated with @AckType.', + todo: 'Ensure the schema variable or getter exists and is annotated with @AckType.', ); } @@ -246,8 +238,7 @@ class AckSchemaGenerator extends Generator { throw InvalidGenerationSource( 'Discriminated base "${model.schemaClassName}" maps multiple discriminator values to subtype "$subtypeSchemaName".', element: element, - todo: - 'Ensure each discriminator value maps to a unique branch schema.', + todo: 'Ensure each discriminator value maps to a unique branch schema.', ); } @@ -288,8 +279,7 @@ class AckSchemaGenerator extends Generator { throw InvalidGenerationSource( 'Extension type generation failed for ${element.name}: $e', element: element, - todo: - 'Ensure nested schemas resolve to @AckType declarations and unsupported schema shapes are not annotated.', + todo: 'Ensure nested schemas resolve to @AckType declarations and unsupported schema shapes are not annotated.', ); } } @@ -328,8 +318,7 @@ class AckSchemaGenerator extends Generator { if (branchIndex == null) { throw InvalidGenerationSource( 'Could not resolve discriminated branch "$branchSchemaClassName" for base "${baseModel.schemaClassName}".', - todo: - 'Ensure every Ack.discriminated(...) branch references an @AckType schema declared in the same library.', + todo: 'Ensure every Ack.discriminated(...) branch references an @AckType schema declared in the same library.', ); } @@ -342,8 +331,7 @@ class AckSchemaGenerator extends Generator { existingOwner != baseModel.schemaClassName) { throw InvalidGenerationSource( 'Branch schema "$branchSchemaClassName" is mapped to multiple discriminated bases: "$existingOwner" and "${baseModel.schemaClassName}".', - todo: - 'A branch schema can only belong to one Ack.discriminated(...) base.', + todo: 'A branch schema can only belong to one Ack.discriminated(...) base.', ); } branchOwnerByCanonicalIdentity[canonicalBranchIdentity] = diff --git a/packages/ack_generator/lib/src/inference/schema_inference.dart b/packages/ack_generator/lib/src/inference/schema_inference.dart index a44727ec..c12752fc 100644 --- a/packages/ack_generator/lib/src/inference/schema_inference.dart +++ b/packages/ack_generator/lib/src/inference/schema_inference.dart @@ -1,5 +1,5 @@ -import 'package:ack_annotations/ack_annotations.dart' as annotations; -import 'package:ack_annotations/format_annotations.dart' as formats; +import 'package:ack/annotations.dart' as annotations; +import 'package:ack/format_annotations.dart' as formats; import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; @@ -18,81 +18,69 @@ final class AckSchemaInference { final String? ackPrefix; - static const _min = TypeChecker.typeNamed( - annotations.Min, - inPackage: 'ack_annotations', - ); - static const _max = TypeChecker.typeNamed( - annotations.Max, - inPackage: 'ack_annotations', - ); + static const _min = TypeChecker.typeNamed(annotations.Min, inPackage: 'ack'); + static const _max = TypeChecker.typeNamed(annotations.Max, inPackage: 'ack'); static const _multipleOf = TypeChecker.typeNamed( annotations.MultipleOf, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _positive = TypeChecker.typeNamed( annotations.Positive, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _negative = TypeChecker.typeNamed( annotations.Negative, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _minLength = TypeChecker.typeNamed( annotations.MinLength, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _maxLength = TypeChecker.typeNamed( annotations.MaxLength, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _pattern = TypeChecker.typeNamed( annotations.Pattern, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _email = TypeChecker.typeNamed( annotations.Email, - inPackage: 'ack_annotations', - ); - static const _url = TypeChecker.typeNamed( - annotations.Url, - inPackage: 'ack_annotations', - ); - static const _uri = TypeChecker.typeNamed( - formats.Uri, - inPackage: 'ack_annotations', + inPackage: 'ack', ); + static const _url = TypeChecker.typeNamed(annotations.Url, inPackage: 'ack'); + static const _uri = TypeChecker.typeNamed(formats.Uri, inPackage: 'ack'); static const _uuid = TypeChecker.typeNamed( annotations.Uuid, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _date = TypeChecker.typeNamed( annotations.Date, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _dateTime = TypeChecker.typeNamed( formats.DateTime, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _notEmpty = TypeChecker.typeNamed( annotations.NotEmpty, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _minItems = TypeChecker.typeNamed( annotations.MinItems, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _maxItems = TypeChecker.typeNamed( annotations.MaxItems, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _uniqueItems = TypeChecker.typeNamed( annotations.UniqueItems, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _ackField = TypeChecker.typeNamed( annotations.AckField, - inPackage: 'ack_annotations', + inPackage: 'ack', ); /// Infers a schema expression for [type]. diff --git a/packages/ack_generator/lib/src/json/ack_json_generator.dart b/packages/ack_generator/lib/src/json/ack_json_generator.dart index e7fe5781..72e3ef17 100644 --- a/packages/ack_generator/lib/src/json/ack_json_generator.dart +++ b/packages/ack_generator/lib/src/json/ack_json_generator.dart @@ -1,5 +1,7 @@ -import 'package:ack_annotations/ack_annotations.dart' show AckModel; -import 'package:ack_annotations/ack_generator_support.dart'; +// ignore_for_file: deprecated_member_use + +import 'package:ack/annotations.dart' show AckModel, Schemable; +import 'package:ack/ack_generator_support.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:build/build.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -23,12 +25,10 @@ final class AckJsonSerializableGenerator extends Generator { static const _marker = TypeChecker.typeNamed( AckGeneratedJson, - inPackage: 'ack_annotations', - ); - static const _model = TypeChecker.typeNamed( - AckModel, - inPackage: 'ack_annotations', + inPackage: 'ack', ); + static const _model = TypeChecker.typeNamed(AckModel, inPackage: 'ack'); + static const _schemable = TypeChecker.typeNamed(Schemable, inPackage: 'ack'); @override String generate(LibraryReader library, BuildStep buildStep) { @@ -43,7 +43,9 @@ final class AckJsonSerializableGenerator extends Generator { } for (final element in library.classes) { - final annotation = _model.firstAnnotationOfExact(element); + final annotation = + _model.firstAnnotationOfExact(element) ?? + _schemable.firstAnnotationOfExact(element); if (annotation == null) continue; final reader = ConstantReader(annotation); if (!element.isSealed) { @@ -58,7 +60,9 @@ final class AckJsonSerializableGenerator extends Generator { (type) => type.element.baseElement == element.baseElement, ); if (!isSubtype) continue; - final branchAnnotation = _model.firstAnnotationOfExact(branch); + final branchAnnotation = + _model.firstAnnotationOfExact(branch) ?? + _schemable.firstAnnotationOfExact(branch); _addModelRequest( requests, claimed, @@ -68,7 +72,9 @@ final class AckJsonSerializableGenerator extends Generator { } } - if (requests.isEmpty) return ''; + if (requests.isEmpty) { + return ''; + } final output = []; for (final request in requests) { diff --git a/packages/ack_generator/lib/src/json/ack_runtime_type_helper.dart b/packages/ack_generator/lib/src/json/ack_runtime_type_helper.dart index 4ff30992..37dbf355 100644 --- a/packages/ack_generator/lib/src/json/ack_runtime_type_helper.dart +++ b/packages/ack_generator/lib/src/json/ack_runtime_type_helper.dart @@ -1,4 +1,6 @@ -import 'package:ack_annotations/ack_annotations.dart' show AckModel; +// ignore_for_file: deprecated_member_use + +import 'package:ack/annotations.dart' show AckModel, Schemable; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:json_serializable/type_helper.dart'; @@ -14,10 +16,8 @@ import 'helper_names.dart'; final class AckRuntimeTypeHelper extends TypeHelper { const AckRuntimeTypeHelper(); - static const _model = TypeChecker.typeNamed( - AckModel, - inPackage: 'ack_annotations', - ); + static const _model = TypeChecker.typeNamed(AckModel, inPackage: 'ack'); + static const _schemable = TypeChecker.typeNamed(Schemable, inPackage: 'ack'); @override Object? serialize( @@ -50,7 +50,10 @@ final class AckRuntimeTypeHelper extends TypeHelper { bool _isClassFirst(ClassElement element) => _model.hasAnnotationOfExact(element) || + _schemable.hasAnnotationOfExact(element) || element.allSupertypes.any( - (type) => _model.hasAnnotationOfExact(type.element), + (type) => + _model.hasAnnotationOfExact(type.element) || + _schemable.hasAnnotationOfExact(type.element), ); } diff --git a/packages/ack_generator/lib/src/model_generator.dart b/packages/ack_generator/lib/src/model_generator.dart index 68dce8d9..bf1b0f0e 100644 --- a/packages/ack_generator/lib/src/model_generator.dart +++ b/packages/ack_generator/lib/src/model_generator.dart @@ -1,4 +1,6 @@ -import 'package:ack_annotations/ack_annotations.dart'; +// ignore_for_file: deprecated_member_use + +import 'package:ack/annotations.dart'; import 'package:ack/ack.dart' show Ack, AckModelAdapter, AckSchema, AckSchemaModel, SchemaResult; import 'package:analyzer/dart/ast/ast.dart'; @@ -18,11 +20,15 @@ import 'models/schema_model_graph.dart'; final class AckModelGenerator extends Generator { static const _ackInferChecker = TypeChecker.typeNamed( AckInfer, - inPackage: 'ack_annotations', + inPackage: 'ack', ); static const _ackModelChecker = TypeChecker.typeNamed( AckModel, - inPackage: 'ack_annotations', + inPackage: 'ack', + ); + static const _schemableChecker = TypeChecker.typeNamed( + Schemable, + inPackage: 'ack', ); static const _ackChecker = TypeChecker.typeNamed(Ack, inPackage: 'ack'); static const _ackModelAdapterChecker = TypeChecker.typeNamed( @@ -69,7 +75,8 @@ final class AckModelGenerator extends Generator { } for (final classElement in library.classes) { - if (_ackModelChecker.hasAnnotationOfExact(classElement)) { + if (_ackModelChecker.hasAnnotationOfExact(classElement) || + _schemableChecker.hasAnnotationOfExact(classElement)) { annotatedModels.add(classElement); } for (final getter in classElement.getters) { @@ -88,14 +95,14 @@ final class AckModelGenerator extends Generator { annotated.isNotEmpty ? annotated.first : annotatedModels.first, ); - ({AckModelGraph graph, String? ackPrefix, String? ackInferPrefix})? + ({AckModelGraph graph, String? ackPrefix, String? schemablePrefix})? schemaFirst; if (annotated.isNotEmpty) { final graph = await SchemaModelGraphBuilder(library).build(annotated); schemaFirst = ( graph: graph, ackPrefix: _ackRuntimeQualifier(library, annotated.first), - ackInferPrefix: _ackInferQualifier(library, annotated.first), + schemablePrefix: _schemableQualifier(library, annotated.first), ); } ({AckModelGraph graph, String? ackPrefix})? classFirst; @@ -121,7 +128,7 @@ final class AckModelGenerator extends Generator { if (schemaFirst != null) { final specs = AckModelEmitter( ackPrefix: schemaFirst.ackPrefix, - ackInferPrefix: schemaFirst.ackInferPrefix, + schemablePrefix: schemaFirst.schemablePrefix, schemaPrefixInScope: library.element.firstFragment.libraryImports.any( (import) => import.prefix?.element.name == 'schema', ), @@ -140,9 +147,8 @@ final class AckModelGenerator extends Generator { } if (classFirst != null) { output.add( - AckClassModelEmitter( - ackPrefix: classFirst.ackPrefix, - ).emit(classFirst.graph), + AckClassModelEmitter(ackPrefix: classFirst.ackPrefix) + .emit(classFirst.graph), ); } return output.where((chunk) => chunk.trim().isNotEmpty).join('\n\n'); @@ -197,7 +203,9 @@ final class AckModelGenerator extends Generator { ); bool _hasAckInfer(Element element) => - _ackInferChecker.hasAnnotationOfExact(element); + _ackInferChecker.hasAnnotationOfExact(element) || + (element is! ClassElement && + _schemableChecker.hasAnnotationOfExact(element)); /// Strips `./` segments so `part './user.ack.dart'` matches the file next to /// the input, without treating `part 'sub/user.ack.dart'` as the same path. @@ -262,23 +270,23 @@ final class AckModelGenerator extends Generator { 'are exposed.', ); - /// Resolves the visible `AckInfer` qualifier for generated JSON markers. + /// Resolves the visible `Schemable` qualifier for generated JSON markers. /// /// Uses import namespaces so barrel re-exports and `show` combinators work. /// Prefixed imports win over unprefixed ones, in import order. - String? _ackInferQualifier( + String? _schemableQualifier( LibraryReader library, Element annotatedElement, ) => _visibleQualifier( library, annotatedElement, - requiredTypes: const {'AckInfer': _ackInferChecker}, + requiredTypes: const {'Schemable': _schemableChecker}, message: - 'Generated @AckInfer.jsonSerializable requires a visible exact AckInfer ' + 'Generated @Schemable.generatedJson requires a visible exact Schemable ' 'import in this library.', todo: - 'Import AckInfer from ack_annotations, using the same prefix as ' - '@AckInfer() when one is present.', + 'Import Schemable from package:ack, using the same prefix as ' + '@Schemable() when one is present.', ); String? _visibleQualifier( diff --git a/packages/ack_generator/lib/src/models/schema_model_graph.dart b/packages/ack_generator/lib/src/models/schema_model_graph.dart index 3c0e9cd9..e9682ea8 100644 --- a/packages/ack_generator/lib/src/models/schema_model_graph.dart +++ b/packages/ack_generator/lib/src/models/schema_model_graph.dart @@ -41,6 +41,7 @@ final class AckConstructorParameter { required this.fieldName, required this.typeRef, this.isSuper = false, + this.isRequired = false, this.defaultExpression, }); @@ -49,6 +50,7 @@ final class AckConstructorParameter { final String fieldName; final AckInferRef typeRef; final bool isSuper; + final bool isRequired; final String? defaultExpression; } @@ -187,6 +189,7 @@ final class AckClassModelMetadata { final String backingName; final String caseStyle; final bool hasExplicitAnnotation; + final List copyWithSupertypes; } diff --git a/packages/ack_generator/pubspec.yaml b/packages/ack_generator/pubspec.yaml index 70302e1a..24617450 100644 --- a/packages/ack_generator/pubspec.yaml +++ b/packages/ack_generator/pubspec.yaml @@ -11,17 +11,17 @@ topics: - schema environment: - sdk: '>=3.9.0 <4.0.0' + sdk: '>=3.13.0 <4.0.0' dependencies: # Core code generation dependencies - analyzer: ">=10.0.0 <11.0.0" + analyzer: ">=13.3.0 <14.0.0" build: ^4.0.0 build_config: ^1.3.0 source_gen: ^4.2.4 code_builder: ^4.10.0 collection: ^1.18.0 - dart_style: ^3.1.0 + dart_style: ^3.1.13 json_annotation: ^4.12.0 json_serializable: ^6.14.1 logging: ^1.3.0 @@ -29,10 +29,9 @@ dependencies: # Ack packages (versions are overridden locally by Melos) ack: ^1.7.0-beta.2 - ack_annotations: ^1.7.0-beta.2 dev_dependencies: - build_runner: ^2.1.7 + build_runner: ^2.15.1 build_test: ^3.1.0 test: ^1.25.15 path: ^1.9.0 diff --git a/packages/ack_generator/pubspec_overrides.yaml b/packages/ack_generator/pubspec_overrides.yaml index da00edc2..573835ab 100644 --- a/packages/ack_generator/pubspec_overrides.yaml +++ b/packages/ack_generator/pubspec_overrides.yaml @@ -1,6 +1,4 @@ -# melos_managed_dependency_overrides: ack,ack_annotations +# melos_managed_dependency_overrides: ack # dependency_overrides: # ack: # path: ../ack -# ack_annotations: -# path: ../ack_annotations diff --git a/packages/ack_generator/test/additional_properties_args_test.dart b/packages/ack_generator/test/additional_properties_args_test.dart index fc31462b..032f12cb 100644 --- a/packages/ack_generator/test/additional_properties_args_test.dart +++ b/packages/ack_generator/test/additional_properties_args_test.dart @@ -39,16 +39,14 @@ final userSchema = Ack.object({ }, ); - test( - 'generates args getter for schema variable with explicit additionalProperties: true', - () async { - final builder = ackGenerator(BuilderOptions.empty); + test('generates args getter for schema variable with explicit additionalProperties: true', () async { + final builder = ackGenerator(BuilderOptions.empty); - await testBuilder( - builder, - { - ...allAssets, - 'test_pkg|lib/schema.dart': ''' + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; @@ -58,29 +56,26 @@ final userSchema = Ack.object({ 'age': Ack.integer(), }, additionalProperties: true); ''', - }, - outputs: { - 'test_pkg|lib/schema.g.dart': decodedMatches( - allOf([ - contains('Map get args =>'), - contains("e.key != 'name' && e.key != 'age'"), - ]), - ), - }, - ); - }, - ); + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + allOf([ + contains('Map get args =>'), + contains("e.key != 'name' && e.key != 'age'"), + ]), + ), + }, + ); + }); - test( - 'does not generate args getter for schema variable without additionalProperties', - () async { - final builder = ackGenerator(BuilderOptions.empty); + test('does not generate args getter for schema variable without additionalProperties', () async { + final builder = ackGenerator(BuilderOptions.empty); - await testBuilder( - builder, - { - ...allAssets, - 'test_pkg|lib/schema.dart': ''' + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; @@ -90,15 +85,14 @@ final userSchema = Ack.object({ 'age': Ack.integer(), }); ''', - }, - outputs: { - 'test_pkg|lib/schema.g.dart': decodedMatches( - isNot(contains('Map get args')), - ), - }, - ); - }, - ); + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + isNot(contains('Map get args')), + ), + }, + ); + }); test( 'generates args getter with no conditions when there are no fields', diff --git a/packages/ack_generator/test/analyzer_constraint_test.dart b/packages/ack_generator/test/analyzer_constraint_test.dart index 79548e12..34a2e615 100644 --- a/packages/ack_generator/test/analyzer_constraint_test.dart +++ b/packages/ack_generator/test/analyzer_constraint_test.dart @@ -4,7 +4,7 @@ import 'package:path/path.dart' as p; import 'package:test/test.dart'; void main() { - test('ack_generator pins Analyzer to the supported 10.x line', () { + test('ack_generator pins Analyzer to the supported 13.x line', () { final pubspec = File( p.join( Directory.current.path.endsWith('ack_generator') @@ -14,10 +14,10 @@ void main() { ), ).readAsStringSync(); - expect(pubspec, contains('analyzer: ">=10.0.0 <11.0.0"')); + expect(pubspec, contains('analyzer: ">=13.3.0 <14.0.0"')); }); - test('schema analysis uses only the supported Analyzer 10 AST shape', () { + test('schema analysis uses only the supported Analyzer 13 AST shape', () { final packageRoot = Directory.current.path.endsWith('ack_generator') ? Directory.current.path : p.join(Directory.current.path, 'packages', 'ack_generator'); @@ -32,6 +32,6 @@ void main() { ).readAsStringSync(); expect(source, isNot(contains('dynamic dynamicArgument'))); - expect(source, isNot(contains('.argumentExpression'))); + expect(source, isNot(contains('NamedExpression'))); }); } diff --git a/packages/ack_generator/test/bugs/schema_variable_bugs_test.dart b/packages/ack_generator/test/bugs/schema_variable_bugs_test.dart index 2c680c09..ada29efa 100644 --- a/packages/ack_generator/test/bugs/schema_variable_bugs_test.dart +++ b/packages/ack_generator/test/bugs/schema_variable_bugs_test.dart @@ -261,12 +261,10 @@ final usersSchema = Ack.list(Ack.object({ }); }); - test( - 'throws on top-level Ack.list(schemaFactory()) when element is not statically resolvable', - () async { - final assets = { - ...allAssets, - 'test_pkg|lib/schema.dart': ''' + test('throws on top-level Ack.list(schemaFactory()) when element is not statically resolvable', () async { + final assets = { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; @@ -275,25 +273,24 @@ schemaFactory() => Ack.string(); @AckType() final usersSchema = Ack.list(schemaFactory()); ''', - }; + }; - await resolveSources(assets, (resolver) async { - final library = await resolver.libraryFor( - AssetId('test_pkg', 'lib/schema.dart'), - ); - final schemaVar = library.topLevelVariables - .whereType() - .firstWhere((e) => e.name == 'usersSchema'); + await resolveSources(assets, (resolver) async { + final library = await resolver.libraryFor( + AssetId('test_pkg', 'lib/schema.dart'), + ); + final schemaVar = library.topLevelVariables + .whereType() + .firstWhere((e) => e.name == 'usersSchema'); - final analyzer = SchemaAstAnalyzer(); + final analyzer = SchemaAstAnalyzer(); - expect( - () => analyzer.analyzeSchemaVariable(schemaVar), - throwsA(isA()), - ); - }); - }, - ); + expect( + () => analyzer.analyzeSchemaVariable(schemaVar), + throwsA(isA()), + ); + }); + }); test('supports prefixed Ack invocations in list schemas', () async { final assets = { @@ -674,12 +671,10 @@ final testSchema = Ack.object({ }); }); - test( - 'throws on Ack.list(schemaFactory()) when element is not statically resolvable', - () async { - final assets = { - ...allAssets, - 'test_pkg|lib/schema.dart': ''' + test('throws on Ack.list(schemaFactory()) when element is not statically resolvable', () async { + final assets = { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; @@ -690,25 +685,24 @@ final testSchema = Ack.object({ 'items': Ack.list(schemaFactory()), }); ''', - }; + }; - await resolveSources(assets, (resolver) async { - final library = await resolver.libraryFor( - AssetId('test_pkg', 'lib/schema.dart'), - ); - final schemaVar = library.topLevelVariables - .whereType() - .firstWhere((e) => e.name == 'testSchema'); + await resolveSources(assets, (resolver) async { + final library = await resolver.libraryFor( + AssetId('test_pkg', 'lib/schema.dart'), + ); + final schemaVar = library.topLevelVariables + .whereType() + .firstWhere((e) => e.name == 'testSchema'); - final analyzer = SchemaAstAnalyzer(); + final analyzer = SchemaAstAnalyzer(); - expect( - () => analyzer.analyzeSchemaVariable(schemaVar), - throwsA(isA()), - ); - }); - }, - ); + expect( + () => analyzer.analyzeSchemaVariable(schemaVar), + throwsA(isA()), + ); + }); + }); test( 'throws when list element method chain exceeds analyzer depth', diff --git a/packages/ack_generator/test/integration/ack_infer_contract_test.dart b/packages/ack_generator/test/integration/ack_infer_contract_test.dart index be98a740..f50502bb 100644 --- a/packages/ack_generator/test/integration/ack_infer_contract_test.dart +++ b/packages/ack_generator/test/integration/ack_infer_contract_test.dart @@ -36,9 +36,9 @@ const _schemaShorthand = r'static final schema = $ack.modelSchema;'; /// static `schema` shorthand. Matcher _schemaShorthandOn(String className, {required bool present}) => predicate((source) { - final start = RegExp( - 'class ${RegExp.escape(className)}\\b', - ).firstMatch(source)?.start; + final start = RegExp('class ${RegExp.escape(className)}\\b') + .firstMatch(source) + ?.start; if (start == null) return false; final end = source.indexOf('\n}\n', start); final body = source.substring(start, end == -1 ? null : end); @@ -64,7 +64,7 @@ final userSchema = Ack.object({ 'test_pkg|lib/schema.ack.dart': decodedMatches( allOf([ contains('final class User'), - contains('@AckInfer.jsonSerializable'), + contains('@Schemable.generatedJson'), contains('factory User.parse(Object? input)'), contains('factory User.fromJson(Map json)'), contains(r'static final $ack = AckModelAdapter'), @@ -106,7 +106,7 @@ final occurredAtSchema = Ack.datetime(); 'test_pkg|lib/schema.ack.dart': decodedMatches( allOf([ contains('final DateTime value;'), - contains('@AckInfer.jsonSerializable'), + contains('@Schemable.generatedJson'), contains('factory OccurredAt.fromJson(String json)'), contains('String toJson()'), contains('SchemaResult safeToJson()'), diff --git a/packages/ack_generator/test/integration/ack_infer_data_class_test.dart b/packages/ack_generator/test/integration/ack_infer_data_class_test.dart index b29ac3dd..999302d2 100644 --- a/packages/ack_generator/test/integration/ack_infer_data_class_test.dart +++ b/packages/ack_generator/test/integration/ack_infer_data_class_test.dart @@ -79,9 +79,11 @@ final class Point with _\$PointAck { }, ); - test('unknown-property modes emit reject, discard, and args capture', () async { - await _build( - ''' + test( + 'unknown-property modes emit reject, discard, and args capture', + () async { + await _build( + ''' $_imports @AckModel() final class Strict with _\$StrictAck { @@ -105,28 +107,31 @@ final class Bag with _\$BagAck { final Map args; } ''', - outputs: { - 'test_pkg|lib/model.ack.dart': decodedMatches( - allOf([ - contains('final _strictObject = Ack.object({'), - isNot( + outputs: { + 'test_pkg|lib/model.ack.dart': decodedMatches( + allOf([ + contains('final _strictObject = Ack.object({'), + isNot( + contains( + "final _strictObject = Ack.object({'name': Ack.string()}, additionalProperties: true)", + ), + ), + contains('additionalProperties: true'), + contains("'args': Map.fromEntries"), + contains('result.remove(\'args\')'), + contains('model.args.entries'), + contains('self.args'), contains( - "final _strictObject = Ack.object({'name': Ack.string()}, additionalProperties: true)", + 'deepUnmodifiableJsonMap(value as Map)', ), - ), - contains('additionalProperties: true'), - contains("'args': Map.fromEntries"), - contains('result.remove(\'args\')'), - contains('model.args.entries'), - contains('self.args'), - contains('deepUnmodifiableJsonMap(value as Map)'), - isNot(contains('_ackClassImmutableCopyValue')), - isNot(contains('_ackClassImmutableCopyMap')), - ]), - ), - }, - ); - }); + isNot(contains('_ackClassImmutableCopyValue')), + isNot(contains('_ackClassImmutableCopyMap')), + ]), + ), + }, + ); + }, + ); test( 'presence overrides and required mixin collisions are diagnosed', diff --git a/packages/ack_generator/test/integration/ack_infer_models_test.dart b/packages/ack_generator/test/integration/ack_infer_models_test.dart index 3a182d15..dce1c900 100644 --- a/packages/ack_generator/test/integration/ack_infer_models_test.dart +++ b/packages/ack_generator/test/integration/ack_infer_models_test.dart @@ -50,7 +50,7 @@ final emptySchema = Ack.object({}); 'test_pkg|lib/empty.ack.dart': decodedMatches( allOf([ contains('Empty()'), - contains('@AckInfer.jsonSerializable'), + contains('@Schemable.generatedJson'), contains(r'_$EmptyFromJson'), contains(r'_$EmptyToJson'), isNot(contains('\n ,')), @@ -459,8 +459,8 @@ final petSchema = Ack.discriminated( contains('sealed class Pet'), contains('final class Cat extends Pet'), contains('final class Dog extends Pet'), - isNot(contains('@AckInfer.jsonSerializable\nsealed class Pet')), - contains('@AckInfer.jsonSerializable\nfinal class Cat extends Pet'), + isNot(contains('@Schemable.generatedJson\nsealed class Pet')), + contains('@Schemable.generatedJson\nfinal class Cat extends Pet'), contains("String get kind => 'cat';"), contains("'kind': 'dog'"), contains('additionalProperties.entries'), @@ -634,7 +634,7 @@ final bagSchema = Ack.object({}).passthrough(); ); }); - test('preserves a prefixed AckInfer qualifier on the JSON marker', () async { + test('preserves a prefixed Schemable qualifier on the JSON marker', () async { await _build( { 'schema.dart': ''' @@ -644,19 +644,19 @@ import 'package:ack_annotations/ack_annotations.dart' as annotations; part 'schema.ack.dart'; part 'schema.ack.g.dart'; -@annotations.AckInfer() +@annotations.Schemable() final userSchema = Ack.object({'name': Ack.string()}); ''', }, outputs: { 'test_pkg|lib/schema.ack.dart': decodedMatches( - contains('@annotations.AckInfer.jsonSerializable'), + contains('@annotations.Schemable.generatedJson'), ), }, ); }); - test('preserves a direct AckInfer qualifier on the JSON marker', () async { + test('preserves a direct Schemable qualifier on the JSON marker', () async { await _build( { 'schema.dart': @@ -671,14 +671,14 @@ final userSchema = Ack.object({'name': Ack.string()}); }, outputs: { 'test_pkg|lib/schema.ack.dart': decodedMatches( - contains('@AckInfer.jsonSerializable'), + contains('@Schemable.generatedJson'), ), }, ); }); test( - 'preserves an unprefixed barrel AckInfer qualifier on the JSON marker', + 'preserves an unprefixed barrel Schemable qualifier on the JSON marker', () async { await _build( { @@ -697,7 +697,7 @@ final userSchema = Ack.object({'name': Ack.string()}); }, outputs: { 'test_pkg|lib/schema.ack.dart': decodedMatches( - contains('@AckInfer.jsonSerializable'), + contains('@Schemable.generatedJson'), ), }, ); @@ -705,7 +705,7 @@ final userSchema = Ack.object({'name': Ack.string()}); ); test( - 'prefers a prefixed AckInfer qualifier when both imports are visible', + 'prefers a prefixed Schemable qualifier when both imports are visible', () async { await _build( { @@ -723,7 +723,7 @@ final userSchema = Ack.object({'name': Ack.string()}); }, outputs: { 'test_pkg|lib/schema.ack.dart': decodedMatches( - contains('@annotations.AckInfer.jsonSerializable'), + contains('@annotations.Schemable.generatedJson'), ), }, ); @@ -731,7 +731,7 @@ final userSchema = Ack.object({'name': Ack.string()}); ); test( - 'preserves a prefixed barrel AckInfer qualifier on the JSON marker', + 'preserves a prefixed barrel Schemable qualifier on the JSON marker', () async { await _build( { @@ -739,18 +739,18 @@ final userSchema = Ack.object({'name': Ack.string()}); "export 'package:ack_annotations/ack_annotations.dart';", 'schema.dart': ''' import 'package:ack/ack.dart'; -import 'annotations.dart' as annotations show AckInfer; +import 'annotations.dart' as annotations show Schemable; part 'schema.ack.dart'; part 'schema.ack.g.dart'; -@annotations.AckInfer() +@annotations.Schemable() final userSchema = Ack.object({'name': Ack.string()}); ''', }, outputs: { 'test_pkg|lib/schema.ack.dart': decodedMatches( - contains('@annotations.AckInfer.jsonSerializable'), + contains('@annotations.Schemable.generatedJson'), ), }, ); diff --git a/packages/ack_generator/test/integration/ack_infer_runtime_build_test.dart b/packages/ack_generator/test/integration/ack_infer_runtime_build_test.dart index 5058c81b..d1a8fdaa 100644 --- a/packages/ack_generator/test/integration/ack_infer_runtime_build_test.dart +++ b/packages/ack_generator/test/integration/ack_infer_runtime_build_test.dart @@ -30,9 +30,8 @@ Future _expectConsumerSafeParts( for (final MapEntry(key: path, value: source) in generated.entries) { expect(source, contains(lintSuppression), reason: path); expect(source, contains('// coverage:ignore-file\n'), reason: path); - File( - p.join(project.path, path), - ).writeAsStringSync(source.replaceFirst(lintSuppression, '')); + File(p.join(project.path, path)) + .writeAsStringSync(source.replaceFirst(lintSuppression, '')); } try { _expectSuccess( @@ -67,9 +66,8 @@ void main() { 'clean generated models compile and preserve the AckInfer runtime contract', () async { var projectRoot = Directory.current; - while (!Directory( - p.join(projectRoot.path, 'packages', 'ack_generator'), - ).existsSync()) { + while (!Directory(p.join(projectRoot.path, 'packages', 'ack_generator')) + .existsSync()) { projectRoot = projectRoot.parent; } final temporary = await Directory.systemTemp.createTemp( @@ -99,18 +97,16 @@ dependency_overrides: ack_annotations: path: ${p.join(projectRoot.path, 'packages', 'ack_annotations')} '''); - File(p.join(temporary.path, 'analysis_options.yaml')).writeAsStringSync( - ''' + File(p.join(temporary.path, 'analysis_options.yaml')) + .writeAsStringSync(''' linter: rules: - avoid_redundant_argument_values - prefer_null_aware_operators -''', - ); - File(p.join(temporary.path, 'lib', 'models.dart')).writeAsStringSync( - r''' +'''); + File(p.join(temporary.path, 'lib', 'models.dart')) + .writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'models.ack.dart'; part 'models.ack.g.dart'; @@ -125,6 +121,7 @@ final class RuntimeUser { final String name; } +// ignore: deprecated_member_use @AckInfer(name: 'UserRecord') final userRecordSchema = Ack.object({ 'name': Ack.string(), @@ -133,7 +130,7 @@ final userRecordSchema = Ack.object({ encode: (user) => {'name': user.name}, ); -@AckInfer() +@Schemable() final AckSchema nodeSchema = Ack.object({ 'label': Ack.string(), 'children': Ack.list( @@ -141,12 +138,12 @@ final AckSchema nodeSchema = Ack.object({ ).optional(), }); -@AckInfer() +@Schemable() final AckSchema authorSchema = Ack.object({ 'books': Ack.list(Ack.lazy('book', () => bookSchema)), }); -@AckInfer() +@Schemable() final AckSchema bookSchema = Ack.object({ 'title': Ack.string(), 'author': Ack.lazy('author', () => authorSchema).optional(), @@ -154,7 +151,7 @@ final AckSchema bookSchema = Ack.object({ const allowExtras = true; -@AckInfer() +@Schemable() final extrasSchema = Ack.object({ 'name': Ack.string(), 'maybe': Ack.string().nullable(), @@ -167,30 +164,30 @@ final extrasSchema = Ack.object({ ), }, additionalProperties: allowExtras); -@AckInfer() +@Schemable() final catSchema = Ack.object({'lives': Ack.integer()}); -@AckInfer() +@Schemable() final dogSchema = Ack.object({'friendly': Ack.boolean()}).passthrough(); -@AckInfer() +@Schemable() final petSchema = Ack.discriminated( discriminatorKey: 'kind', schemas: {'cat': catSchema, 'dog': dogSchema}, ); -@AckInfer(name: 'MemberType') +@Schemable(name: 'MemberType') final memberSchema = Ack.string(); -@AckInfer() +@Schemable() final emptySchema = Ack.object({}); -@AckInfer() +@Schemable() final scoresSchema = Ack.list(Ack.integer()); final looseAny = Ack.any(); -@AckInfer() +@Schemable() final envelopeSchema = Ack.object({ 'kind': Ack.any(), 'payload': Ack.any().nullable().optional(), @@ -203,7 +200,7 @@ final envelopeSchema = Ack.object({ final nullableLabel = Ack.string().nullable(); -@AckInfer() +@Schemable() final labelsSchema = Ack.object({ 'byKey': Ack.map(nullableLabel), 'groups': Ack.map(Ack.list(Ack.string()).nullable()), @@ -224,34 +221,34 @@ final class Counted { } } -@AckInfer() +@Schemable() final documentSchema = Ack.object({'schema': Ack.string()}); -@AckInfer(name: 'Settings') +@Schemable(name: 'Settings') final schema = Ack.object({'theme': Ack.string()}); -@AckInfer() +@Schemable() final draftSchema = Ack.object({'body': Ack.string()}); -@AckInfer() +@Schemable() final revisionSchema = Ack.discriminated( discriminatorKey: 'schema', schemas: {'draft': draftSchema}, ); -@AckInfer() +@Schemable() final circleSchema = Ack.object({'radius': Ack.integer()}); -@AckInfer() +@Schemable() final ruleSchema = Ack.object({'schema': Ack.string()}); -@AckInfer() +@Schemable() final shapeSchema = Ack.discriminated( discriminatorKey: 'type', schemas: {'circle': circleSchema, 'rule': ruleSchema}, ); -@AckInfer(name: 'CountedModel') +@Schemable(name: 'CountedModel') final countedSchema = Ack.object({ 'item': Ack.string().codec( decode: Counted.decode, @@ -264,27 +261,22 @@ final countedSchema = Ack.object({ ), ), }); -''', - ); - File(p.join(temporary.path, 'lib', 'address.dart')).writeAsStringSync( - r''' +'''); + File(p.join(temporary.path, 'lib', 'address.dart')) + .writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'address.ack.dart'; part 'address.ack.g.dart'; -@AckInfer() +@Schemable() final addressSchema = Ack.object({'city': Ack.string()}); -''', - ); - File( - p.join(temporary.path, 'lib', 'exports.dart'), - ).writeAsStringSync("export 'address.dart';\n"); - File(p.join(temporary.path, 'lib', 'person.dart')).writeAsStringSync( - r''' +'''); + File(p.join(temporary.path, 'lib', 'exports.dart')) + .writeAsStringSync("export 'address.dart';\n"); + File(p.join(temporary.path, 'lib', 'person.dart')) + .writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; import 'address.dart' as direct; import 'exports.dart' as exported; @@ -292,16 +284,14 @@ import 'exports.dart' as exported; part 'person.ack.dart'; part 'person.ack.g.dart'; -@AckInfer() +@Schemable() final personSchema = Ack.object({ 'home': direct.addressSchema, 'history': Ack.list(exported.addressSchema), }); -''', - ); - File( - p.join(temporary.path, 'test', 'runtime_test.dart'), - ).writeAsStringSync(r''' +'''); + File(p.join(temporary.path, 'test', 'runtime_test.dart')) + .writeAsStringSync(r''' import 'package:ack/ack.dart'; import 'package:ack_ack_infer_runtime/models.dart'; import 'package:ack_ack_infer_runtime/person.dart'; diff --git a/packages/ack_generator/test/integration/ack_type_cross_file_resolution_test.dart b/packages/ack_generator/test/integration/ack_type_cross_file_resolution_test.dart index 36d33c20..f78ecbe0 100644 --- a/packages/ack_generator/test/integration/ack_type_cross_file_resolution_test.dart +++ b/packages/ack_generator/test/integration/ack_type_cross_file_resolution_test.dart @@ -299,16 +299,14 @@ final themeSchema = Ack.object({ ); }); - test( - 'resolves prefixed transformed refs without @AckType using visible representation types', - () async { - final builder = ackGenerator(BuilderOptions.empty); + test('resolves prefixed transformed refs without @AckType using visible representation types', () async { + final builder = ackGenerator(BuilderOptions.empty); - await testBuilder( - builder, - { - ...allAssets, - 'test_pkg|lib/palette_schemas.dart': ''' + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/palette_schemas.dart': ''' import 'package:ack/ack.dart'; class Color { @@ -318,7 +316,7 @@ class Color { final colorSchema = Ack.string().transform((value) => Color(value)); ''', - 'test_pkg|lib/theme_schemas.dart': ''' + 'test_pkg|lib/theme_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; import 'palette_schemas.dart' as palette; @@ -329,39 +327,36 @@ final themeSchema = Ack.object({ 'colors': Ack.list(palette.colorSchema), }); ''', - }, - outputs: { - 'test_pkg|lib/theme_schemas.g.dart': decodedMatches( - allOf([ - contains('palette.Color get accent'), - contains("_data['accent'] as palette.Color"), - contains('List get colors'), - contains("_\$ackListCast(_data['colors'])"), - ]), - ), - }, - ); - }, - ); + }, + outputs: { + 'test_pkg|lib/theme_schemas.g.dart': decodedMatches( + allOf([ + contains('palette.Color get accent'), + contains("_data['accent'] as palette.Color"), + contains('List get colors'), + contains("_\$ackListCast(_data['colors'])"), + ]), + ), + }, + ); + }); - test( - 'fails for direct-import transformed refs when representation types are not visible', - () async { - final builder = ackGenerator(BuilderOptions.empty); + test('fails for direct-import transformed refs when representation types are not visible', () async { + final builder = ackGenerator(BuilderOptions.empty); - await expectGenerationFailure( - builder: builder, - expectedMessage: 'is not visible from this library', - expectedOutputs: {'test_pkg|lib/palette_schemas.g.dart': anything}, - assets: { - ...allAssets, - 'test_pkg|lib/hidden_types.dart': ''' + await expectGenerationFailure( + builder: builder, + expectedMessage: 'is not visible from this library', + expectedOutputs: {'test_pkg|lib/palette_schemas.g.dart': anything}, + assets: { + ...allAssets, + 'test_pkg|lib/hidden_types.dart': ''' class HiddenColor { final String value; const HiddenColor(this.value); } ''', - 'test_pkg|lib/palette_schemas.dart': ''' + 'test_pkg|lib/palette_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; @@ -371,7 +366,7 @@ import 'hidden_types.dart'; final hiddenColorSchema = Ack.string() .transform((value) => HiddenColor(value)); ''', - 'test_pkg|lib/theme_schemas.dart': ''' + 'test_pkg|lib/theme_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; import 'palette_schemas.dart'; @@ -381,21 +376,18 @@ final themeSchema = Ack.object({ 'accent': hiddenColorSchema, }); ''', - }, - ); - }, - ); + }, + ); + }); - test( - 'resolves prefixed transformed generic refs when representation types are exported', - () async { - final builder = ackGenerator(BuilderOptions.empty); + test('resolves prefixed transformed generic refs when representation types are exported', () async { + final builder = ackGenerator(BuilderOptions.empty); - await testBuilder( - builder, - { - ...allAssets, - 'test_pkg|lib/palette_schemas.dart': ''' + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/palette_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; @@ -413,7 +405,7 @@ class Box { final boxedColorSchema = Ack.string().transform>((value) => Box(Color(value))); ''', - 'test_pkg|lib/theme_schemas.dart': ''' + 'test_pkg|lib/theme_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; import 'palette_schemas.dart' as palette; @@ -434,40 +426,37 @@ final themeSchema = Ack.object({ 'colors': Ack.list(palette.boxedColorSchema), }); ''', - }, - outputs: { - 'test_pkg|lib/palette_schemas.g.dart': decodedMatches( - contains('extension type BoxedColorType(Box _value)'), - ), - 'test_pkg|lib/theme_schemas.g.dart': decodedMatches( - allOf([ - contains('palette.BoxedColorType get accent'), - contains( - "palette.BoxedColorType(_data['accent'] as palette.Box)", - ), - contains('List get colors'), - contains( - 'palette.BoxedColorType(e as palette.Box)', - ), - ]), - ), - }, - ); - }, - ); + }, + outputs: { + 'test_pkg|lib/palette_schemas.g.dart': decodedMatches( + contains('extension type BoxedColorType(Box _value)'), + ), + 'test_pkg|lib/theme_schemas.g.dart': decodedMatches( + allOf([ + contains('palette.BoxedColorType get accent'), + contains( + "palette.BoxedColorType(_data['accent'] as palette.Box)", + ), + contains('List get colors'), + contains( + 'palette.BoxedColorType(e as palette.Box)', + ), + ]), + ), + }, + ); + }); - test( - 'fails for direct-import transformed refs when representation types collide locally', - () async { - final builder = ackGenerator(BuilderOptions.empty); + test('fails for direct-import transformed refs when representation types collide locally', () async { + final builder = ackGenerator(BuilderOptions.empty); - await expectGenerationFailure( - builder: builder, - expectedMessage: 'is ambiguous in this library', - expectedOutputs: {'test_pkg|lib/palette_schemas.g.dart': anything}, - assets: { - ...allAssets, - 'test_pkg|lib/palette_schemas.dart': ''' + await expectGenerationFailure( + builder: builder, + expectedMessage: 'is ambiguous in this library', + expectedOutputs: {'test_pkg|lib/palette_schemas.g.dart': anything}, + assets: { + ...allAssets, + 'test_pkg|lib/palette_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; @@ -479,7 +468,7 @@ class Color { @AckType() final colorSchema = Ack.string().transform((value) => Color(value)); ''', - 'test_pkg|lib/theme_schemas.dart': ''' + 'test_pkg|lib/theme_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; import 'palette_schemas.dart'; @@ -494,23 +483,20 @@ final themeSchema = Ack.object({ 'accent': colorSchema, }); ''', - }, - ); - }, - ); + }, + ); + }); - test( - 'fails for direct-import transformed refs when multiple imports expose the representation type', - () async { - final builder = ackGenerator(BuilderOptions.empty); + test('fails for direct-import transformed refs when multiple imports expose the representation type', () async { + final builder = ackGenerator(BuilderOptions.empty); - await expectGenerationFailure( - builder: builder, - expectedMessage: 'is ambiguous in this library', - expectedOutputs: {'test_pkg|lib/palette_schemas.g.dart': anything}, - assets: { - ...allAssets, - 'test_pkg|lib/palette_schemas.dart': ''' + await expectGenerationFailure( + builder: builder, + expectedMessage: 'is ambiguous in this library', + expectedOutputs: {'test_pkg|lib/palette_schemas.g.dart': anything}, + assets: { + ...allAssets, + 'test_pkg|lib/palette_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; @@ -522,13 +508,13 @@ class Color { @AckType() final colorSchema = Ack.string().transform((value) => Color(value)); ''', - 'test_pkg|lib/alt_color_types.dart': ''' + 'test_pkg|lib/alt_color_types.dart': ''' class Color { final String localValue; const Color(this.localValue); } ''', - 'test_pkg|lib/theme_schemas.dart': ''' + 'test_pkg|lib/theme_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; import 'alt_color_types.dart'; @@ -539,29 +525,26 @@ final themeSchema = Ack.object({ 'accent': colorSchema, }); ''', - }, - ); - }, - ); + }, + ); + }); - test( - 'fails for prefixed transformed refs when representation types are not visible', - () async { - final builder = ackGenerator(BuilderOptions.empty); + test('fails for prefixed transformed refs when representation types are not visible', () async { + final builder = ackGenerator(BuilderOptions.empty); - await expectGenerationFailure( - builder: builder, - expectedMessage: 'is not visible from this library', - expectedOutputs: {'test_pkg|lib/palette_schemas.g.dart': anything}, - assets: { - ...allAssets, - 'test_pkg|lib/hidden_types.dart': ''' + await expectGenerationFailure( + builder: builder, + expectedMessage: 'is not visible from this library', + expectedOutputs: {'test_pkg|lib/palette_schemas.g.dart': anything}, + assets: { + ...allAssets, + 'test_pkg|lib/hidden_types.dart': ''' class HiddenColor { final String value; const HiddenColor(this.value); } ''', - 'test_pkg|lib/palette_schemas.dart': ''' + 'test_pkg|lib/palette_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; @@ -571,7 +554,7 @@ import 'hidden_types.dart'; final hiddenColorSchema = Ack.string() .transform((value) => HiddenColor(value)); ''', - 'test_pkg|lib/theme_schemas.dart': ''' + 'test_pkg|lib/theme_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; import 'palette_schemas.dart' as palette; @@ -581,30 +564,26 @@ final themeSchema = Ack.object({ 'accent': palette.hiddenColorSchema, }); ''', - }, - ); - }, - ); + }, + ); + }); - test( - 'fails for cross-file transformed refs that use qualified representation types', - () async { - final builder = ackGenerator(BuilderOptions.empty); + test('fails for cross-file transformed refs that use qualified representation types', () async { + final builder = ackGenerator(BuilderOptions.empty); - await expectGenerationFailure( - builder: builder, - expectedMessage: - 'uses a qualified type that cannot be referenced across library boundaries', - expectedOutputs: {'test_pkg|lib/palette_schemas.g.dart': anything}, - assets: { - ...allAssets, - 'test_pkg|lib/hidden_types.dart': ''' + await expectGenerationFailure( + builder: builder, + expectedMessage: 'uses a qualified type that cannot be referenced across library boundaries', + expectedOutputs: {'test_pkg|lib/palette_schemas.g.dart': anything}, + assets: { + ...allAssets, + 'test_pkg|lib/hidden_types.dart': ''' class HiddenColor { final String value; const HiddenColor(this.value); } ''', - 'test_pkg|lib/palette_schemas.dart': ''' + 'test_pkg|lib/palette_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; @@ -614,7 +593,7 @@ import 'hidden_types.dart' as dep; final hiddenColorSchema = Ack.string() .transform((value) => dep.HiddenColor(value)); ''', - 'test_pkg|lib/theme_schemas.dart': ''' + 'test_pkg|lib/theme_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; import 'palette_schemas.dart' as palette; @@ -624,10 +603,9 @@ final themeSchema = Ack.object({ 'accent': palette.hiddenColorSchema, }); ''', - }, - ); - }, - ); + }, + ); + }); test('supports @AckType alias schema declarations', () async { final builder = ackGenerator(BuilderOptions.empty); @@ -673,16 +651,14 @@ final deckToolArgsSchema = Ack.object({ ); }); - test( - 'resolves typed nested getters for schema references with optional/nullable modifiers', - () async { - final builder = ackGenerator(BuilderOptions.empty); + test('resolves typed nested getters for schema references with optional/nullable modifiers', () async { + final builder = ackGenerator(BuilderOptions.empty); - await testBuilder( - builder, - { - ...allAssets, - 'test_pkg|lib/deck_schemas.dart': ''' + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/deck_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; @@ -691,7 +667,7 @@ final slideSchema = Ack.object({ 'id': Ack.string(), }); ''', - 'test_pkg|lib/deck_tools_schemas.dart': ''' + 'test_pkg|lib/deck_tools_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; import 'deck_schemas.dart'; @@ -702,33 +678,32 @@ final deckToolArgsSchema = Ack.object({ 'selectedSlide': slideSchema.nullable(), }); ''', - }, - outputs: { - 'test_pkg|lib/deck_schemas.g.dart': decodedMatches( - contains('extension type SlideType(Map _data)'), - ), - 'test_pkg|lib/deck_tools_schemas.g.dart': decodedMatches( - allOf([ - contains('SlideType? get currentSlide'), - contains( - "SlideType? get currentSlide => _data['currentSlide'] != null", - ), - contains( - "? SlideType(_data['currentSlide'] as Map)", - ), - contains('SlideType? get selectedSlide'), - contains( - "SlideType? get selectedSlide => _data['selectedSlide'] != null", - ), - contains( - "? SlideType(_data['selectedSlide'] as Map)", - ), - ]), - ), - }, - ); - }, - ); + }, + outputs: { + 'test_pkg|lib/deck_schemas.g.dart': decodedMatches( + contains('extension type SlideType(Map _data)'), + ), + 'test_pkg|lib/deck_tools_schemas.g.dart': decodedMatches( + allOf([ + contains('SlideType? get currentSlide'), + contains( + "SlideType? get currentSlide => _data['currentSlide'] != null", + ), + contains( + "? SlideType(_data['currentSlide'] as Map)", + ), + contains('SlideType? get selectedSlide'), + contains( + "SlideType? get selectedSlide => _data['selectedSlide'] != null", + ), + contains( + "? SlideType(_data['selectedSlide'] as Map)", + ), + ]), + ), + }, + ); + }); test('fails when nested object schema reference is unresolved', () async { final builder = ackGenerator(BuilderOptions.empty); @@ -763,17 +738,15 @@ final deckToolArgsSchema = Ack.object({ ); }); - test( - 'fails when prefixed schema reference does not exist in that prefix namespace', - () async { - final builder = ackGenerator(BuilderOptions.empty); + test('fails when prefixed schema reference does not exist in that prefix namespace', () async { + final builder = ackGenerator(BuilderOptions.empty); - await expectLater( - () => testBuilder( - builder, - { - ...allAssets, - 'test_pkg|lib/a_schemas.dart': ''' + await expectLater( + () => testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/a_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; @@ -782,7 +755,7 @@ final aOnlySchema = Ack.object({ 'id': Ack.string(), }); ''', - 'test_pkg|lib/b_schemas.dart': ''' + 'test_pkg|lib/b_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; @@ -791,7 +764,7 @@ final slideSchema = Ack.object({ 'id': Ack.string(), }); ''', - 'test_pkg|lib/deck_tools_schemas.dart': ''' + 'test_pkg|lib/deck_tools_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; import 'a_schemas.dart' as a; @@ -803,14 +776,13 @@ final deckToolArgsSchema = Ack.object({ 'known': slideSchema, }); ''', - }, - outputs: {'test_pkg|lib/deck_tools_schemas.g.dart': anything}, - ), - // Generator emits: 'Could not resolve schema reference "slideSchema"' - throwsA(isA()), - ); - }, - ); + }, + outputs: {'test_pkg|lib/deck_tools_schemas.g.dart': anything}, + ), + // Generator emits: 'Could not resolve schema reference "slideSchema"' + throwsA(isA()), + ); + }); test('fails when nested object schema reference lacks @AckType', () async { final builder = ackGenerator(BuilderOptions.empty); @@ -846,16 +818,14 @@ final deckToolArgsSchema = Ack.object({ ); }); - test( - 'resolves direct-import transformed refs without @AckType using visible representation types', - () async { - final builder = ackGenerator(BuilderOptions.empty); + test('resolves direct-import transformed refs without @AckType using visible representation types', () async { + final builder = ackGenerator(BuilderOptions.empty); - await testBuilder( - builder, - { - ...allAssets, - 'test_pkg|lib/palette.dart': ''' + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/palette.dart': ''' import 'package:ack/ack.dart'; class Color { @@ -865,7 +835,7 @@ class Color { final colorSchema = Ack.string().transform((value) => Color(value)); ''', - 'test_pkg|lib/theme_schemas.dart': ''' + 'test_pkg|lib/theme_schemas.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; import 'palette.dart' as palette; @@ -876,20 +846,19 @@ final themeSchema = Ack.object({ 'accents': Ack.list(palette.colorSchema), }); ''', - }, - outputs: { - 'test_pkg|lib/theme_schemas.g.dart': decodedMatches( - allOf([ - contains('palette.Color get primary'), - contains("_data['primary'] as palette.Color"), - contains('List get accents'), - contains("_\$ackListCast(_data['accents'])"), - ]), - ), - }, - ); - }, - ); + }, + outputs: { + 'test_pkg|lib/theme_schemas.g.dart': decodedMatches( + allOf([ + contains('palette.Color get primary'), + contains("_data['primary'] as palette.Color"), + contains('List get accents'), + contains("_\$ackListCast(_data['accents'])"), + ]), + ), + }, + ); + }); test( 'fails when Ack.list(schemaRef) object reference lacks @AckType', diff --git a/packages/ack_generator/test/integration/ack_type_discriminated_test.dart b/packages/ack_generator/test/integration/ack_type_discriminated_test.dart index 2b2e8acc..bfb54112 100644 --- a/packages/ack_generator/test/integration/ack_type_discriminated_test.dart +++ b/packages/ack_generator/test/integration/ack_type_discriminated_test.dart @@ -8,16 +8,14 @@ import '../test_utils/test_assets.dart'; void main() { group('@AckType discriminated schemas', () { - test( - 'generates discriminated subtypes when branches omit discriminator property', - () async { - final builder = ackGenerator(BuilderOptions.empty); + test('generates discriminated subtypes when branches omit discriminator property', () async { + final builder = ackGenerator(BuilderOptions.empty); - await testBuilder( - builder, - { - ...allAssets, - 'test_pkg|lib/schema.dart': ''' + await testBuilder( + builder, + { + ...allAssets, + 'test_pkg|lib/schema.dart': ''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; @@ -40,43 +38,42 @@ final petSchema = Ack.discriminated( }, ); ''', - }, - outputs: { - 'test_pkg|lib/schema.g.dart': decodedMatches( - allOf([ - contains('extension type PetType(Map _data)'), - contains('implements Map'), - contains("switch (map['kind'])"), - contains("'cat' => CatType(map)"), - contains("'dog' => DogType(map)"), - contains('extension type CatType(Map _data)'), - contains('extension type DogType(Map _data)'), - contains('implements PetType, Map'), - contains('return petSchema.parseAs('), - contains('return petSchema.safeParseAs('), - contains(".effectiveBranch('cat')"), - contains(".effectiveBranch('dog')"), - isNot(contains('return catSchema.parseAs(')), - isNot(contains('return catSchema.safeParseAs(')), - isNot(contains('return dogSchema.parseAs(')), - isNot(contains('return dogSchema.safeParseAs(')), - isNot(contains("map['kind'] !=")), - isNot(contains('Expected kind')), - contains('Map get args =>'), - contains("e.key != 'kind' && e.key != 'bark'"), - predicate((content) { - final source = content as String; - final count = RegExp( - r"String get kind => _data\['kind'\] as String;", - ).allMatches(source).length; - return count == 3; - }, 'contains one kind getter per generated type'), - ]), - ), - }, - ); - }, - ); + }, + outputs: { + 'test_pkg|lib/schema.g.dart': decodedMatches( + allOf([ + contains('extension type PetType(Map _data)'), + contains('implements Map'), + contains("switch (map['kind'])"), + contains("'cat' => CatType(map)"), + contains("'dog' => DogType(map)"), + contains('extension type CatType(Map _data)'), + contains('extension type DogType(Map _data)'), + contains('implements PetType, Map'), + contains('return petSchema.parseAs('), + contains('return petSchema.safeParseAs('), + contains(".effectiveBranch('cat')"), + contains(".effectiveBranch('dog')"), + isNot(contains('return catSchema.parseAs(')), + isNot(contains('return catSchema.safeParseAs(')), + isNot(contains('return dogSchema.parseAs(')), + isNot(contains('return dogSchema.safeParseAs(')), + isNot(contains("map['kind'] !=")), + isNot(contains('Expected kind')), + contains('Map get args =>'), + contains("e.key != 'kind' && e.key != 'bark'"), + predicate((content) { + final source = content as String; + final count = RegExp( + r"String get kind => _data\['kind'\] as String;", + ).allMatches(source).length; + return count == 3; + }, 'contains one kind getter per generated type'), + ]), + ), + }, + ); + }); test('allows existing matching discriminator literal', () async { final builder = ackGenerator(BuilderOptions.empty); diff --git a/packages/ack_generator/test/integration/class_first_cycle_build_test.dart b/packages/ack_generator/test/integration/class_first_cycle_build_test.dart index e1d25ec6..b777da11 100644 --- a/packages/ack_generator/test/integration/class_first_cycle_build_test.dart +++ b/packages/ack_generator/test/integration/class_first_cycle_build_test.dart @@ -14,9 +14,8 @@ void main() { 'a clean build rejects a two-library class-first cycle with a field path', () async { var projectRoot = Directory.current; - while (!Directory( - p.join(projectRoot.path, 'packages', 'ack_generator'), - ).existsSync()) { + while (!Directory(p.join(projectRoot.path, 'packages', 'ack_generator')) + .existsSync()) { projectRoot = projectRoot.parent; } final temporary = await Directory.systemTemp.createTemp( diff --git a/packages/ack_generator/test/integration/class_first_nested_runtime_build_test.dart b/packages/ack_generator/test/integration/class_first_nested_runtime_build_test.dart index f7ff121d..a869d0ae 100644 --- a/packages/ack_generator/test/integration/class_first_nested_runtime_build_test.dart +++ b/packages/ack_generator/test/integration/class_first_nested_runtime_build_test.dart @@ -36,9 +36,8 @@ void main() { 'class-first facades compose across libraries from a clean build', () async { var projectRoot = Directory.current; - while (!Directory( - p.join(projectRoot.path, 'packages', 'ack_generator'), - ).existsSync()) { + while (!Directory(p.join(projectRoot.path, 'packages', 'ack_generator')) + .existsSync()) { projectRoot = projectRoot.parent; } final temporary = await Directory.systemTemp.createTemp( @@ -68,15 +67,14 @@ dependency_overrides: ack_annotations: path: ${p.join(projectRoot.path, 'packages', 'ack_annotations')} '''); - File(p.join(temporary.path, 'lib', 'address.dart')).writeAsStringSync( - r''' + File(p.join(temporary.path, 'lib', 'address.dart')) + .writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'address.ack.dart'; part 'address.ack.g.dart'; -@AckModel(schemaName: 'PostalAddressSchema') +@Schemable(schemaName: 'PostalAddressSchema') final class Address with _$AddressAck { const Address({required this.city}); @@ -85,26 +83,24 @@ final class Address with _$AddressAck { static final fromJson = PostalAddressSchema.fromJson; } -@AckModel() +@Schemable() final class AddressBook with _$AddressBookAck { const AddressBook({required this.primary, this.secondary}); final Address primary; final Address? secondary; } -''', - ); - File(p.join(temporary.path, 'lib', 'customer.dart')).writeAsStringSync( - r''' +'''); + File(p.join(temporary.path, 'lib', 'customer.dart')) + .writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; import 'address.dart' show Address, PostalAddressSchema; part 'customer.ack.dart'; part 'customer.ack.g.dart'; -@AckModel() +@Schemable() final class Customer with _$CustomerAck { const Customer({ this.primary = const Address(city: 'Default City'), @@ -114,37 +110,32 @@ final class Customer with _$CustomerAck { final Address primary; final Address? secondary; } -''', - ); - File( - p.join(temporary.path, 'lib', 'models.dart'), - ).writeAsStringSync("export 'address.dart';\n"); - File(p.join(temporary.path, 'lib', 'parcel.dart')).writeAsStringSync( - r''' +'''); + File(p.join(temporary.path, 'lib', 'models.dart')) + .writeAsStringSync("export 'address.dart';\n"); + File(p.join(temporary.path, 'lib', 'parcel.dart')) + .writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; import 'models.dart' as models; part 'parcel.ack.dart'; part 'parcel.ack.g.dart'; -@AckModel() +@Schemable() final class Parcel with _$ParcelAck { const Parcel({required this.destination}); final models.Address destination; } -''', - ); +'''); File(p.join(temporary.path, 'lib', 'north.dart')).writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'north.ack.dart'; part 'north.ack.g.dart'; -@AckModel() +@Schemable() final class Place with _$PlaceAck { const Place({required this.name}); @@ -153,22 +144,20 @@ final class Place with _$PlaceAck { '''); File(p.join(temporary.path, 'lib', 'south.dart')).writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'south.ack.dart'; part 'south.ack.g.dart'; -@AckModel() +@Schemable() final class Place with _$PlaceAck { const Place({required this.name}); final String name; } '''); - File(p.join(temporary.path, 'lib', 'itinerary.dart')).writeAsStringSync( - r''' + File(p.join(temporary.path, 'lib', 'itinerary.dart')) + .writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; import 'north.dart' as north; import 'south.dart' as south; @@ -176,23 +165,21 @@ import 'south.dart' as south; part 'itinerary.ack.dart'; part 'itinerary.ack.g.dart'; -@AckModel() +@Schemable() final class Itinerary with _$ItineraryAck { const Itinerary({required this.start, required this.finish}); final north.Place start; final south.Place finish; } -''', - ); +'''); File(p.join(temporary.path, 'lib', 'pet.dart')).writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'pet.ack.dart'; part 'pet.ack.g.dart'; -@AckModel(discriminatorKey: 'type') +@Schemable(discriminatorKey: 'type') sealed class Pet with _$PetAck { const Pet({required this.id}); @@ -207,7 +194,6 @@ final class Cat extends Pet with _$CatAck { '''); File(p.join(temporary.path, 'lib', 'order.dart')).writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; import 'address.dart' as address; import 'pet.dart' as pets; @@ -215,7 +201,7 @@ import 'pet.dart' as pets; part 'order.ack.dart'; part 'order.ack.g.dart'; -@AckModel() +@Schemable() final class Order with _$OrderAck { const Order({ required this.shipping, @@ -232,29 +218,26 @@ final class Order with _$OrderAck { final pets.Cat cat; } '''); - File(p.join(temporary.path, 'lib', 'legacy.dart')).writeAsStringSync( - r''' + File(p.join(temporary.path, 'lib', 'legacy.dart')) + .writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'legacy.ack.dart'; part 'legacy.ack.g.dart'; -@AckInfer(name: 'LegacyAddress') +@Schemable(name: 'LegacyAddress') final legacyAddressContract = Ack.object({'city': Ack.string()}); -''', - ); - File(p.join(temporary.path, 'lib', 'holder.dart')).writeAsStringSync( - r''' +'''); + File(p.join(temporary.path, 'lib', 'holder.dart')) + .writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; import 'legacy.dart' as legacy; part 'holder.ack.dart'; part 'holder.ack.g.dart'; -@AckModel() +@Schemable() final class Holder with _$HolderAck { const Holder({ required this.primary, @@ -270,20 +253,17 @@ final class Holder with _$HolderAck { final List> matrix; final Set unique; } -''', - ); - File( - p.join(temporary.path, 'lib', 'address_envelope.dart'), - ).writeAsStringSync(r''' +'''); + File(p.join(temporary.path, 'lib', 'address_envelope.dart')) + .writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; import 'address.dart' as address; part 'address_envelope.ack.dart'; part 'address_envelope.ack.g.dart'; -@AckInfer() +@Schemable() final addressEnvelopeSchema = Ack.object({ 'primary': address.PostalAddressSchema.schema, 'optional': address.PostalAddressSchema.schema.optional(), @@ -295,9 +275,8 @@ final addressEnvelopeSchema = Ack.object({ ), }); '''); - File( - p.join(temporary.path, 'test', 'nested_test.dart'), - ).writeAsStringSync(r''' + File(p.join(temporary.path, 'test', 'nested_test.dart')) + .writeAsStringSync(r''' import 'package:ack_class_first_nested/address.dart'; import 'package:ack_class_first_nested/address_envelope.dart'; import 'package:ack_class_first_nested/customer.dart'; diff --git a/packages/ack_generator/test/integration/class_first_runtime_build_test.dart b/packages/ack_generator/test/integration/class_first_runtime_build_test.dart index 1aacd597..5034a90b 100644 --- a/packages/ack_generator/test/integration/class_first_runtime_build_test.dart +++ b/packages/ack_generator/test/integration/class_first_runtime_build_test.dart @@ -48,9 +48,8 @@ Future _expectConsumerSafeParts( for (final MapEntry(key: path, value: source) in modern.entries) { expect(source, contains(lintSuppression), reason: path); expect(source, contains('// coverage:ignore-file\n'), reason: path); - File( - p.join(project.path, path), - ).writeAsStringSync(source.replaceFirst(lintSuppression, '')); + File(p.join(project.path, path)) + .writeAsStringSync(source.replaceFirst(lintSuppression, '')); } try { _expectSuccess( @@ -85,9 +84,8 @@ void main() { 'class-first models compile and preserve the runtime contract', () async { var projectRoot = Directory.current; - while (!Directory( - p.join(projectRoot.path, 'packages', 'ack_generator'), - ).existsSync()) { + while (!Directory(p.join(projectRoot.path, 'packages', 'ack_generator')) + .existsSync()) { projectRoot = projectRoot.parent; } final temporary = await Directory.systemTemp.createTemp( @@ -100,7 +98,7 @@ void main() { name: ack_class_first_runtime publish_to: none environment: - sdk: '>=3.9.0 <4.0.0' + sdk: '>=3.13.0 <4.0.0' dependencies: ack: path: ${p.join(projectRoot.path, 'packages', 'ack')} @@ -119,14 +117,13 @@ dependency_overrides: ack_annotations: path: ${p.join(projectRoot.path, 'packages', 'ack_annotations')} '''); - File(p.join(temporary.path, 'analysis_options.yaml')).writeAsStringSync( - ''' + File(p.join(temporary.path, 'analysis_options.yaml')) + .writeAsStringSync(''' linter: rules: - avoid_redundant_argument_values - prefer_null_aware_operators -''', - ); +'''); File(p.join(temporary.path, 'build.yaml')).writeAsStringSync(''' targets: \$default: @@ -134,6 +131,7 @@ targets: ack_generator:ack_generator: generate_for: - lib/coexist.dart + - lib/factories.dart source_gen:combining_builder: generate_for: - lib/models.dart @@ -144,10 +142,9 @@ targets: File(p.join(temporary.path, 'lib', 'beta.dart')).writeAsStringSync( 'final class Item { const Item(this.value); final int value; }\n', ); - File(p.join(temporary.path, 'lib', 'coexist.dart')).writeAsStringSync( - r''' + File(p.join(temporary.path, 'lib', 'coexist.dart')) + .writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'coexist.g.dart'; part 'coexist.ack.dart'; @@ -157,21 +154,52 @@ part 'coexist.ack.g.dart'; @AckType() final frozenSchema = Ack.object({'id': Ack.string()}); -@AckInfer() +@Schemable() final modernSchema = Ack.object({'name': Ack.string()}); +// ignore: deprecated_member_use @AckModel() final class Handwritten with _$HandwrittenAck { const Handwritten({required this.enabled}); final bool enabled; } -''', - ); - File(p.join(temporary.path, 'lib', 'models.dart')).writeAsStringSync( - r''' +'''); + File(p.join(temporary.path, 'lib', 'factories.dart')) + .writeAsStringSync(r''' +import 'package:ack/ack.dart'; + +part 'factories.ack.dart'; +part 'factories.ack.g.dart'; + +@Schemable(description: 'A habit date.') +final class HabitDateDto({ + @AckField(description: 'The calendar day.') + required final DateTime value, +}) with _$HabitDateDtoAck; + +@Schemable(description: 'A habit entry.') +final class HabitDto({ + required final String name, + required final HabitDateDto date, + final String? note, +}) with _$HabitDtoAck; + +@Schemable(unknownProperties: AckUnknownPropertyPolicy.capture) +final class OpenDto({ + required final String name, + required final Map additionalProperties, +}) with _$OpenDtoAck; + +@Schemable(description: 'A unified annotation model.') +final class const SchemableDto({ + @MinLength(2) required final String name, + final String label = 'default', +}) with _$SchemableDtoAck; +'''); + File(p.join(temporary.path, 'lib', 'models.dart')) + .writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; import 'package:json_annotation/json_annotation.dart' show JsonSerializable; import 'alpha.dart' as alpha; @@ -212,7 +240,7 @@ AckSchema, Map>> groupsSchema() => encode: (value) => value, ); -@AckModel() +@Schemable() final class Profile with _$ProfileAck { const Profile({ required this.name, @@ -235,7 +263,7 @@ final class Profile with _$ProfileAck { static final fromJson = ProfileSchema.fromJson; } -@AckModel(caseStyle: AckCaseStyle.snake) +@Schemable(caseStyle: AckCaseStyle.snake) final class Account with _$AccountAck { const Account({required this.firstName, required this.imageUrl}); final String firstName; @@ -245,7 +273,7 @@ final class Account with _$AccountAck { static final fromJson = AccountSchema.fromJson; } -@AckModel(unknownProperties: AckUnknownPropertyPolicy.capture) +@Schemable(unknownProperties: AckUnknownPropertyPolicy.capture) final class Config with _$ConfigAck { const Config({ required this.name, @@ -255,7 +283,7 @@ final class Config with _$ConfigAck { final Map additionalProperties; } -@AckModel( +@Schemable( caseStyle: AckCaseStyle.snake, unknownProperties: AckUnknownPropertyPolicy.capture, captureField: 'extraValues', @@ -269,13 +297,13 @@ final class CaseStyledExtras with _$CaseStyledExtrasAck { final Map extraValues; } -@AckModel(unknownProperties: AckUnknownPropertyPolicy.discard) +@Schemable(unknownProperties: AckUnknownPropertyPolicy.discard) final class Loose with _$LooseAck { const Loose({required this.name}); final String name; } -@AckModel() +@Schemable() final class Normalized with _$NormalizedAck { const Normalized(String? value) : value = value ?? ''; @@ -283,21 +311,21 @@ final class Normalized with _$NormalizedAck { final String value; } -@AckModel() +@Schemable() final class NullableDefault with _$NullableDefaultAck { const NullableDefault({this.label = 'fallback'}); final String? label; } -@AckModel() +@Schemable() final class NullDefault with _$NullDefaultAck { const NullDefault({this.label = null}); final String? label; } -@AckModel() +@Schemable() final class ImportedPair with _$ImportedPairAck { const ImportedPair({required this.left, required this.right}); @AckField(schema: alphaItemSchema) @@ -306,7 +334,7 @@ final class ImportedPair with _$ImportedPairAck { final beta.Item right; } -@AckModel() +@Schemable() final class ImmutableCollections with _$ImmutableCollectionsAck { const ImmutableCollections({ required this.matrix, @@ -321,7 +349,7 @@ final class ImmutableCollections with _$ImmutableCollectionsAck { final Map> groups; } -@AckModel() +@Schemable() final class Example with _$ExampleAck { const Example({this.label, this.title}); @@ -337,14 +365,14 @@ final class Example with _$ExampleAck { static final fromJson = ExampleSchema.fromJson; } -@AckModel() +@Schemable() final class ExampleHolder with _$ExampleHolderAck { const ExampleHolder({required this.example}); final Example example; } -@AckModel() +@Schemable() final class OptionalNullable with _$OptionalNullableAck { const OptionalNullable({this.note}); @@ -352,7 +380,7 @@ final class OptionalNullable with _$OptionalNullableAck { final String? note; } -@AckModel() +@Schemable() final class InferredNotNull with _$InferredNotNullAck { const InferredNotNull({this.label}); @@ -362,7 +390,7 @@ final class InferredNotNull with _$InferredNotNullAck { AckSchema nullableNameSchema() => Ack.string().nullable(); -@AckModel() +@Schemable() final class OverrideNotNull with _$OverrideNotNullAck { const OverrideNotNull({this.name}); @@ -371,7 +399,7 @@ final class OverrideNotNull with _$OverrideNotNullAck { final String? name; } -@AckModel() +@Schemable() final class RequiredNotNull with _$RequiredNotNullAck { const RequiredNotNull({this.value}); @@ -397,7 +425,7 @@ parameterMapSchema() => }, ); -@AckModel() +@Schemable() final class CapabilityBinding with _$CapabilityBindingAck { CapabilityBinding({ required this.name, @@ -416,7 +444,7 @@ final class CapabilityBinding with _$CapabilityBindingAck { AckSchema, Map> scoresSchema() => Ack.map(Ack.integer().min(0)); -@AckModel() +@Schemable() final class Envelope with _$EnvelopeAck { const Envelope({ required this.kind, @@ -443,12 +471,12 @@ final class Envelope with _$EnvelopeAck { final Map scores; } -@AckModel() +@Schemable() final class Declined with _$DeclinedAck { const Declined(); } -@AckModel() +@Schemable() final class OptionalCollections with _$OptionalCollectionsAck { const OptionalCollections({this.headers, this.aliases, this.tags}); @@ -457,13 +485,13 @@ final class OptionalCollections with _$OptionalCollectionsAck { final Set? tags; } -@AckModel(discriminatorKey: 'type') +@Schemable(discriminatorKey: 'type') sealed class Pet with _$PetAck { const Pet({required this.id}); final String id; } -@AckModel(discriminatorValue: 'cat') +@Schemable(discriminatorValue: 'cat') final class Cat extends Pet with _$CatAck { const Cat({required super.id, required this.lives}); final int lives; @@ -475,7 +503,7 @@ final class Dog extends Pet with _$DogAck { String get type => 'Dog'; } -@AckModel(discriminatorKey: 'type') +@Schemable(discriminatorKey: 'type') sealed class Animal with _$AnimalAck { const Animal({required this.id}); final String id; @@ -491,19 +519,19 @@ final class BabyRabbit extends Rabbit with _$BabyRabbitAck { final String? note; } -@AckModel() +@Schemable() final class Parent with _$ParentAck { const Parent({required this.id}); final String id; } -@AckModel() +@Schemable() final class Child extends Parent with _$ChildAck { const Child({required super.id, this.note}); final String? note; } -@AckModel() +@Schemable() final class ImplementingChild with _$ImplementingChildAck implements Parent { const ImplementingChild({required this.id, this.note}); final String id; @@ -512,7 +540,7 @@ final class ImplementingChild with _$ImplementingChildAck implements Parent { /// A task the person can complete. /// @description This tag has lower priority. -@AckModel(description: 'A task the person can complete.') +@Schemable(description: 'A task the person can complete.') final class Task with _$TaskAck { const Task({required this.id, required this.title}); @@ -528,7 +556,7 @@ final class Task with _$TaskAck { * related tasks. * @description A board that groups related tasks. */ -@AckModel() +@Schemable() final class Board with _$BoardAck { const Board({required this.focus, required this.backlog, this.pinned}); @@ -540,7 +568,7 @@ final class Board with _$BoardAck { final Task? pinned; } -@AckModel() +@Schemable() final class Undocumented with _$UndocumentedAck { const Undocumented({required this.value}); @@ -550,13 +578,13 @@ final class Undocumented with _$UndocumentedAck { /// A shape to draw. /// @description A shape to draw. -@AckModel(discriminatorKey: 'kind') +@Schemable(discriminatorKey: 'kind') sealed class Shape with _$ShapeAck { const Shape(); } /// A circle, sized by its radius. -@AckModel( +@Schemable( discriminatorValue: 'circle', description: 'A circle, sized by its radius.', ) @@ -572,7 +600,7 @@ final class Square extends Shape with _$SquareAck { final double side; } -@AckInfer() +@Schemable() final legacySchema = Ack.object({'enabled': Ack.boolean()}); @JsonSerializable() @@ -583,16 +611,15 @@ final class PlainJson { final String value; Map toJson() => _$PlainJsonToJson(this); } -''', - ); - File( - p.join(temporary.path, 'test', 'runtime_test.dart'), - ).writeAsStringSync(r''' +'''); + File(p.join(temporary.path, 'test', 'runtime_test.dart')) + .writeAsStringSync(r''' import 'dart:convert'; import 'package:ack_class_first_runtime/alpha.dart' as alpha; import 'package:ack_class_first_runtime/beta.dart' as beta; import 'package:ack_class_first_runtime/coexist.dart'; +import 'package:ack_class_first_runtime/factories.dart'; import 'package:ack_class_first_runtime/models.dart'; import 'package:ack/ack.dart'; import 'package:test/test.dart'; @@ -621,6 +648,52 @@ final envelopeJson = { }; void main() { + test('class-first models generate from constructor-backed fields', () { + final habit = HabitDtoSchema.parse({ + 'name': 'Read', + 'date': {'value': '2026-09-30T00:00:00.000Z'}, + }); + expect(habit, isA()); + expect(habit.name, 'Read'); + expect(habit.date, isA()); + expect(habit.date.value, DateTime.parse('2026-09-30T00:00:00.000Z')); + expect(habit.toJson(), { + 'name': 'Read', + 'date': {'value': '2026-09-30T00:00:00.000Z'}, + }); + expect(habit.copyWith(name: 'Write').name, 'Write'); + expect(habit.copyWith(note: 'keep').copyWith(note: null).note, isNull); + expect(HabitDtoSchema.toJsonSchema()['description'], 'A habit entry.'); + final properties = + HabitDateDtoSchema.toJsonSchema()['properties']! as Map; + expect((properties['value']! as Map)['description'], + 'The calendar day.'); + }); + + test('class-first models capture unknown properties', () { + final model = OpenDtoSchema.parse({ + 'name': 'Read', + 'color': 'blue', + }); + expect(model.additionalProperties, {'color': 'blue'}); + expect(model.toJson(), { + 'name': 'Read', + 'color': 'blue', + }); + }); + + test('Schemable supports schema-first and class-first generation', () { + expect(Modern.parse({'name': 'Ada'}).name, 'Ada'); + final model = SchemableDtoSchema.parse({'name': 'Ada'}); + expect(model.name, 'Ada'); + expect(model.label, 'default'); + expect(model.toJson(), {'name': 'Ada', 'label': 'default'}); + expect(model.copyWith(label: 'changed').label, 'changed'); + expect(() => SchemableDtoSchema.parse({'name': 'A'}), throwsA(isA())); + expect(SchemableDtoSchema.toJsonSchema()['description'], + 'A unified annotation model.'); + }); + test('presence, defaults, collections, and escape hatches round-trip', () { final profile = Profile.fromJson({ 'name': 'Ada', @@ -1244,6 +1317,10 @@ import 'models.dart'; Profile misuse(Profile profile) => profile.copyWith(nickname: const Object()); '''); _expectSuccess(await _run(temporary, ['test']), 'dart test'); + final factories = File(p.join(temporary.path, 'lib', 'factories.dart')); + factories.writeAsStringSync( + '${factories.readAsStringSync()}\n// Rebuild.\n', + ); _expectSuccess( await _run(temporary, ['run', 'build_runner', 'build']), 'outputs-present build_runner build', diff --git a/packages/ack_generator/test/integration/class_model_emitter_test.dart b/packages/ack_generator/test/integration/class_model_emitter_test.dart index 8ad4772a..b48a4393 100644 --- a/packages/ack_generator/test/integration/class_model_emitter_test.dart +++ b/packages/ack_generator/test/integration/class_model_emitter_test.dart @@ -376,11 +376,13 @@ final class Envelope with _\$EnvelopeAck { ); }); - test('emits a codec schema, presence semantics, mixin, and bridges', () async { - await _build( - { - 'profile.dart': - ''' + test( + 'emits a codec schema, presence semantics, mixin, and bridges', + () async { + await _build( + { + 'profile.dart': + ''' $_imports part 'profile.ack.dart'; part 'profile.ack.g.dart'; @@ -405,73 +407,76 @@ final class Profile with _\$ProfileAck { final Set tags; } ''', - }, - outputs: { - 'test_pkg|lib/profile.ack.dart': decodedMatches( - allOf([ - contains('final _profileObject = Ack.object'), - contains( - 'final _profileWireSchema = ' - 'Ack.preserveBoundary(_profileObject)', - ), - contains('final _profileSchema = _profileObject.codec'), - contains('get wireSchema'), - contains('_profileWireSchema;'), - isNot(contains('final profileSchema =')), - contains("'bio': Ack.string().minLength(1).maxLength(500)"), - contains("'website': Ack.uri().optional().nullable()"), - contains("'nickname': Ack.string().nullable()"), - contains("'role': Ack.string().withDefault('member')"), - contains('Ack.list(Ack.string())'), - contains( - RegExp( - r'\.minItems\(1\)[\s\S]*\.unique\(\)[\s\S]*\.codec>', + }, + outputs: { + 'test_pkg|lib/profile.ack.dart': decodedMatches( + allOf([ + contains('final _profileObject = Ack.object'), + contains( + 'final _profileWireSchema = ' + 'Ack.preserveBoundary(_profileObject)', ), - ), - contains('.codec('), - contains(r'decode: _$ProfileFromRuntime'), - contains(r'encode: _$ProfileToRuntime'), - contains('abstract final class ProfileSchema'), - contains( - 'static AckSchema, Profile> get schema', - ), - contains('static Profile parse('), - contains('static SchemaResult safeParse('), - contains( - 'static Profile fromJson(Map json) => parse(json)', - ), - contains('static Map encode('), - contains('static SchemaResult> safeEncode('), - contains('static Map toJsonSchema()'), - contains('static AckSchemaModel toSchemaModel()'), - contains('_profileSchema.parse(value, debugName: debugName)!'), - contains('_profileSchema.encode(value, debugName: debugName)!'), - contains(r'Profile _$ProfileFromRuntime'), - contains(r'_$ProfileFromJson'), - contains(r'Map _$ProfileToRuntime'), - contains("result['nickname'] = null"), - contains(r'mixin _$ProfileAck'), - contains(r'$ProfileCopyWith get copyWith'), - contains('final class _ProfileCopyWithUnset'), - contains('const _ProfileCopyWithUnset()'), - contains('static const _ProfileCopyWithUnset _ackCopyWithUnset ='), - contains('Object? website = _ackCopyWithUnset'), - contains('website: identical(website, _ackCopyWithUnset)'), - contains(': website as Uri?'), - isNot(contains('_ackCopyWithOmitted')), - contains('deepEquals('), - contains('deepHashCode('), - contains('Map toJson()'), - contains('SchemaResult> safeToJson()'), - contains('ProfileSchema.encode(this as Profile)'), - contains('ProfileSchema.safeEncode(this as Profile)'), - contains('_ackProfileFromRuntimeBio'), - contains('_ackProfileToRuntimeTags'), - ]), - ), - }, - ); - }); + contains('final _profileSchema = _profileObject.codec'), + contains('get wireSchema'), + contains('_profileWireSchema;'), + isNot(contains('final profileSchema =')), + contains("'bio': Ack.string().minLength(1).maxLength(500)"), + contains("'website': Ack.uri().optional().nullable()"), + contains("'nickname': Ack.string().nullable()"), + contains("'role': Ack.string().withDefault('member')"), + contains('Ack.list(Ack.string())'), + contains( + RegExp( + r'\.minItems\(1\)[\s\S]*\.unique\(\)[\s\S]*\.codec>', + ), + ), + contains('.codec('), + contains(r'decode: _$ProfileFromRuntime'), + contains(r'encode: _$ProfileToRuntime'), + contains('abstract final class ProfileSchema'), + contains( + 'static AckSchema, Profile> get schema', + ), + contains('static Profile parse('), + contains('static SchemaResult safeParse('), + contains( + 'static Profile fromJson(Map json) => parse(json)', + ), + contains('static Map encode('), + contains('static SchemaResult> safeEncode('), + contains('static Map toJsonSchema()'), + contains('static AckSchemaModel toSchemaModel()'), + contains('_profileSchema.parse(value, debugName: debugName)!'), + contains('_profileSchema.encode(value, debugName: debugName)!'), + contains(r'Profile _$ProfileFromRuntime'), + contains(r'_$ProfileFromJson'), + contains(r'Map _$ProfileToRuntime'), + contains("result['nickname'] = null"), + contains(r'mixin _$ProfileAck'), + contains(r'$ProfileCopyWith get copyWith'), + contains('final class _ProfileCopyWithUnset'), + contains('const _ProfileCopyWithUnset()'), + contains( + 'static const _ProfileCopyWithUnset _ackCopyWithUnset =', + ), + contains('Object? website = _ackCopyWithUnset'), + contains('website: identical(website, _ackCopyWithUnset)'), + contains(': website as Uri?'), + isNot(contains('_ackCopyWithOmitted')), + contains('deepEquals('), + contains('deepHashCode('), + contains('Map toJson()'), + contains('SchemaResult> safeToJson()'), + contains('ProfileSchema.encode(this as Profile)'), + contains('ProfileSchema.safeEncode(this as Profile)'), + contains('_ackProfileFromRuntimeBio'), + contains('_ackProfileToRuntimeTags'), + ]), + ), + }, + ); + }, + ); test( 'emits escape-hatch schemas and built-in recursive type coverage', diff --git a/packages/ack_generator/test/integration/class_model_graph_test.dart b/packages/ack_generator/test/integration/class_model_graph_test.dart index 65017476..982ced37 100644 --- a/packages/ack_generator/test/integration/class_model_graph_test.dart +++ b/packages/ack_generator/test/integration/class_model_graph_test.dart @@ -64,6 +64,16 @@ part 'model.ack.g.dart'; '''; void main() { + test('rejects mutable primary-constructor fields', () async { + await _expectFailure( + r''' +@Schemable() +final class User({required var String name}) with _$UserAck; +''', + ['User.name must be final'], + ); + }); + test( 'rejects a local declaration named like the copyWith implementation', () async { diff --git a/packages/ack_generator/test/integration/example_folder_build_test.dart b/packages/ack_generator/test/integration/example_folder_build_test.dart index af879cf4..6b2fc676 100644 --- a/packages/ack_generator/test/integration/example_folder_build_test.dart +++ b/packages/ack_generator/test/integration/example_folder_build_test.dart @@ -59,9 +59,8 @@ void main() { 'example clean-builds, analyzes, tests, and regenerates deterministically', () async { var projectRoot = Directory.current; - while (!Directory( - p.join(projectRoot.path, 'packages', 'ack_generator'), - ).existsSync()) { + while (!Directory(p.join(projectRoot.path, 'packages', 'ack_generator')) + .existsSync()) { projectRoot = projectRoot.parent; } final sourceExample = Directory(p.join(projectRoot.path, 'example')); @@ -74,15 +73,14 @@ void main() { try { _copyDirectory(sourceExample, temporaryExample); - File( - p.join(temporaryExample.path, 'analysis_options.yaml'), - ).writeAsStringSync(''' + File(p.join(temporaryExample.path, 'analysis_options.yaml')) + .writeAsStringSync(''' analyzer: language: strict-casts: true '''); - File(p.join(temporaryExample.path, 'pubspec.yaml')).writeAsStringSync( - ''' + File(p.join(temporaryExample.path, 'pubspec.yaml')) + .writeAsStringSync(''' name: ack_example publish_to: none environment: @@ -102,8 +100,7 @@ dependency_overrides: path: ${p.join(projectRoot.path, 'packages', 'ack')} ack_annotations: path: ${p.join(projectRoot.path, 'packages', 'ack_annotations')} -''', - ); +'''); _expectSuccess( await _run(temporaryExample, ['pub', 'get']), @@ -148,7 +145,7 @@ dependency_overrides: expect(entry.value, isNot(contains('final accountSchema'))); } else { expect(entry.value, contains('class ')); - expect(entry.value, contains('jsonSerializable')); + expect(entry.value, contains('Schemable.generatedJson')); } expect(entry.value, isNot(contains('extension type'))); expect(entry.value, isNot(contains('fromMap'))); @@ -181,34 +178,29 @@ dependency_overrides: timeout: const Timeout(Duration(minutes: 3)), ); - test( - 'rebuilds when generated outputs are already present', - () async { - var projectRoot = Directory.current; - while (!Directory( - p.join(projectRoot.path, 'packages', 'ack_generator'), - ).existsSync()) { - projectRoot = projectRoot.parent; - } - final sourceExample = Directory(p.join(projectRoot.path, 'example')); - final temporaryRoot = await Directory.systemTemp.createTemp( - 'ack_generator_example_present_', - ); - final temporaryExample = Directory( - p.join(temporaryRoot.path, 'ack_example'), - ); + test('rebuilds when generated outputs are already present', () async { + var projectRoot = Directory.current; + while (!Directory(p.join(projectRoot.path, 'packages', 'ack_generator')) + .existsSync()) { + projectRoot = projectRoot.parent; + } + final sourceExample = Directory(p.join(projectRoot.path, 'example')); + final temporaryRoot = await Directory.systemTemp.createTemp( + 'ack_generator_example_present_', + ); + final temporaryExample = Directory( + p.join(temporaryRoot.path, 'ack_example'), + ); - try { - _copyDirectory(sourceExample, temporaryExample, keepGenerated: true); - File( - p.join(temporaryExample.path, 'analysis_options.yaml'), - ).writeAsStringSync(''' + try { + _copyDirectory(sourceExample, temporaryExample, keepGenerated: true); + File(p.join(temporaryExample.path, 'analysis_options.yaml')) + .writeAsStringSync(''' analyzer: language: strict-casts: true '''); - File(p.join(temporaryExample.path, 'pubspec.yaml')).writeAsStringSync( - ''' + File(p.join(temporaryExample.path, 'pubspec.yaml')).writeAsStringSync(''' name: ack_example publish_to: none environment: @@ -228,45 +220,42 @@ dependency_overrides: path: ${p.join(projectRoot.path, 'packages', 'ack')} ack_annotations: path: ${p.join(projectRoot.path, 'packages', 'ack_annotations')} -''', - ); +'''); - _expectSuccess( - await _run(temporaryExample, ['pub', 'get']), - 'dart pub get', - ); + _expectSuccess( + await _run(temporaryExample, ['pub', 'get']), + 'dart pub get', + ); - final schemaFile = File( - p.join(temporaryExample.path, 'lib', 'schema_types_simple.dart'), - ); - schemaFile.writeAsStringSync( - schemaFile.readAsStringSync().replaceFirst( - "'name': Ack.string(),", - "'name': Ack.string(),\n 'nickname': Ack.string().optional(),", - ), - ); - final generatedFile = File( - p.join(temporaryExample.path, 'lib', 'schema_types_simple.ack.dart'), - ); - final before = generatedFile.readAsStringSync(); + final schemaFile = File( + p.join(temporaryExample.path, 'lib', 'schema_types_simple.dart'), + ); + schemaFile.writeAsStringSync( + schemaFile.readAsStringSync().replaceFirst( + "'name': Ack.string(),", + "'name': Ack.string(),\n 'nickname': Ack.string().optional(),", + ), + ); + final generatedFile = File( + p.join(temporaryExample.path, 'lib', 'schema_types_simple.ack.dart'), + ); + final before = generatedFile.readAsStringSync(); - _expectSuccess( - await _run(temporaryExample, ['run', 'build_runner', 'build']), - 'outputs-present build_runner build', - ); + _expectSuccess( + await _run(temporaryExample, ['run', 'build_runner', 'build']), + 'outputs-present build_runner build', + ); - final after = generatedFile.readAsStringSync(); - expect(after, isNot(equals(before))); - expect(after, contains('nickname')); - _expectSuccess( - await _run(temporaryExample, ['analyze', '--fatal-infos']), - 'dart analyze --fatal-infos', - ); - _expectSuccess(await _run(temporaryExample, ['test']), 'dart test'); - } finally { - temporaryRoot.deleteSync(recursive: true); - } - }, - timeout: const Timeout(Duration(minutes: 3)), - ); + final after = generatedFile.readAsStringSync(); + expect(after, isNot(equals(before))); + expect(after, contains('nickname')); + _expectSuccess( + await _run(temporaryExample, ['analyze', '--fatal-infos']), + 'dart analyze --fatal-infos', + ); + _expectSuccess(await _run(temporaryExample, ['test']), 'dart test'); + } finally { + temporaryRoot.deleteSync(recursive: true); + } + }, timeout: const Timeout(Duration(minutes: 3))); } diff --git a/packages/ack_generator/test/integration/inference_library_test.dart b/packages/ack_generator/test/integration/inference_library_test.dart index e278e4b1..240cd937 100644 --- a/packages/ack_generator/test/integration/inference_library_test.dart +++ b/packages/ack_generator/test/integration/inference_library_test.dart @@ -52,8 +52,7 @@ void main() { await readerWriter.testing.loadIsolateSources(); await testBuilder( const _InferenceProbe( - sourceComment: - '/// The item title.\r\n/// @description It appears on screen.\r\n/// More prose.', + sourceComment: '/// The item title.\r\n/// @description It appears on screen.\r\n/// More prose.', ), { 'test_pkg|lib/widget.dart': ''' diff --git a/packages/ack_generator/test/integration/json_serializable_build_test.dart b/packages/ack_generator/test/integration/json_serializable_build_test.dart index 331d0d46..69461aa5 100644 --- a/packages/ack_generator/test/integration/json_serializable_build_test.dart +++ b/packages/ack_generator/test/integration/json_serializable_build_test.dart @@ -8,9 +8,8 @@ import 'package:test/test.dart'; Directory _projectRoot() { var projectRoot = Directory.current; - while (!Directory( - p.join(projectRoot.path, 'packages', 'ack_generator'), - ).existsSync()) { + while (!Directory(p.join(projectRoot.path, 'packages', 'ack_generator')) + .existsSync()) { projectRoot = projectRoot.parent; } return projectRoot; @@ -37,16 +36,14 @@ int _helperDefinitionCount(String source, String className, String suffix) { } void main() { - test( - 'ack_generator alone generates json_serializable Ack helpers', - () async { - final projectRoot = _projectRoot(); - final temporary = await Directory.systemTemp.createTemp( - 'ack_json_ack_only_', - ); - try { - Directory(p.join(temporary.path, 'lib')).createSync(); - File(p.join(temporary.path, 'pubspec.yaml')).writeAsStringSync(''' + test('ack_generator alone generates json_serializable Ack helpers', () async { + final projectRoot = _projectRoot(); + final temporary = await Directory.systemTemp.createTemp( + 'ack_json_ack_only_', + ); + try { + Directory(p.join(temporary.path, 'lib')).createSync(); + File(p.join(temporary.path, 'pubspec.yaml')).writeAsStringSync(''' name: ack_json_ack_only publish_to: none environment: @@ -66,52 +63,47 @@ dependency_overrides: ack_annotations: path: ${p.join(projectRoot.path, 'packages', 'ack_annotations')} '''); - File(p.join(temporary.path, 'lib', 'user.dart')).writeAsStringSync(r''' + File(p.join(temporary.path, 'lib', 'user.dart')).writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; part 'user.ack.dart'; part 'user.ack.g.dart'; -@AckInfer() +@Schemable() final userSchema = Ack.object({ 'name': Ack.string(), 'createdAt': Ack.datetime(), }); '''); - _expectSuccess(await _run(temporary, ['pub', 'get']), 'dart pub get'); - _expectSuccess( - await _run(temporary, ['run', 'build_runner', 'build']), - 'clean build_runner build', - ); + _expectSuccess(await _run(temporary, ['pub', 'get']), 'dart pub get'); + _expectSuccess( + await _run(temporary, ['run', 'build_runner', 'build']), + 'clean build_runner build', + ); - final ackPart = File( - p.join(temporary.path, 'lib', 'user.ack.dart'), - ).readAsStringSync(); - final jsonPart = File( - p.join(temporary.path, 'lib', 'user.ack.g.dart'), - ).readAsStringSync(); + final ackPart = File(p.join(temporary.path, 'lib', 'user.ack.dart')) + .readAsStringSync(); + final jsonPart = File(p.join(temporary.path, 'lib', 'user.ack.g.dart')) + .readAsStringSync(); - expect(ackPart, contains('@AckInfer.jsonSerializable')); - expect(ackPart, contains(r'_$UserFromJson')); - expect(ackPart, contains('_ackFromRuntimeCreatedAt')); - expect(jsonPart, contains('JsonSerializableGenerator')); - expect(jsonPart, contains('User._ackFromRuntimeName(json[\'name\'])')); - expect( - jsonPart, - contains('User._ackFromRuntimeCreatedAt(json[\'createdAt\'])'), - ); - expect(jsonPart, contains('User._ackToRuntimeName(instance.name)')); - expect(_helperDefinitionCount(jsonPart, 'User', 'FromJson'), 1); - expect(_helperDefinitionCount(jsonPart, 'User', 'ToJson'), 1); - expect(jsonPart, isNot(contains("value['name']"))); - } finally { - temporary.deleteSync(recursive: true); - } - }, - timeout: const Timeout(Duration(minutes: 3)), - ); + expect(ackPart, contains('@Schemable.generatedJson')); + expect(ackPart, contains(r'_$UserFromJson')); + expect(ackPart, contains('_ackFromRuntimeCreatedAt')); + expect(jsonPart, contains('JsonSerializableGenerator')); + expect(jsonPart, contains('User._ackFromRuntimeName(json[\'name\'])')); + expect( + jsonPart, + contains('User._ackFromRuntimeCreatedAt(json[\'createdAt\'])'), + ); + expect(jsonPart, contains('User._ackToRuntimeName(instance.name)')); + expect(_helperDefinitionCount(jsonPart, 'User', 'FromJson'), 1); + expect(_helperDefinitionCount(jsonPart, 'User', 'ToJson'), 1); + expect(jsonPart, isNot(contains("value['name']"))); + } finally { + temporary.deleteSync(recursive: true); + } + }, timeout: const Timeout(Duration(minutes: 3))); test( 'ordinary json_serializable coexists without duplicate Ack helpers', @@ -161,14 +153,13 @@ targets: '''); File(p.join(temporary.path, 'lib', 'same.dart')).writeAsStringSync(r''' import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; import 'package:json_annotation/json_annotation.dart'; part 'same.ack.dart'; part 'same.ack.g.dart'; part 'same.g.dart'; -@AckInfer() +@Schemable() final userSchema = Ack.object({ 'name': Ack.string(), 'nickname': Ack.string().optional(), @@ -184,9 +175,8 @@ final class SameEnvelope { Map toJson() => _$SameEnvelopeToJson(this); } '''); - File( - p.join(temporary.path, 'test', 'runtime_test.dart'), - ).writeAsStringSync(r''' + File(p.join(temporary.path, 'test', 'runtime_test.dart')) + .writeAsStringSync(r''' import 'package:ack_json_coexist/same.dart'; import 'package:test/test.dart'; @@ -211,12 +201,10 @@ void main() { 'dart analyze --fatal-infos', ); - final ackJson = File( - p.join(temporary.path, 'lib', 'same.ack.g.dart'), - ).readAsStringSync(); - final ordinaryJson = File( - p.join(temporary.path, 'lib', 'same.g.dart'), - ).readAsStringSync(); + final ackJson = File(p.join(temporary.path, 'lib', 'same.ack.g.dart')) + .readAsStringSync(); + final ordinaryJson = File(p.join(temporary.path, 'lib', 'same.g.dart')) + .readAsStringSync(); expect(_helperDefinitionCount(ackJson, 'User', 'FromJson'), 1); expect(_helperDefinitionCount(ackJson, 'User', 'ToJson'), 1); expect(_helperDefinitionCount(ordinaryJson, 'User', 'FromJson'), 0); @@ -270,9 +258,8 @@ dependency_overrides: ack_annotations: path: ${p.join(projectRoot.path, 'packages', 'ack_annotations')} '''); - File( - p.join(temporary.path, 'lib', 'role.dart'), - ).writeAsStringSync('enum Role { admin, member }\n'); + File(p.join(temporary.path, 'lib', 'role.dart')) + .writeAsStringSync('enum Role { admin, member }\n'); File(p.join(temporary.path, 'lib', 'support.dart')).writeAsStringSync( "export 'package:ack/ack.dart';\n" "export 'package:ack_annotations/ack_annotations.dart';\n" @@ -284,15 +271,14 @@ import 'support.dart' as support; part 'user.ack.dart'; part 'user.ack.g.dart'; -@support.AckInfer() +@support.Schemable() final userSchema = support.Ack.object({ 'name': support.Ack.string(), 'role': support.Ack.enumValues(support.Role.values), }); '''); - File( - p.join(temporary.path, 'test', 'runtime_test.dart'), - ).writeAsStringSync(r''' + File(p.join(temporary.path, 'test', 'runtime_test.dart')) + .writeAsStringSync(r''' import 'package:ack_json_barrel/user.dart'; import 'package:test/test.dart'; @@ -316,10 +302,9 @@ void main() { 'dart analyze --fatal-infos', ); - final ackPart = File( - p.join(temporary.path, 'lib', 'user.ack.dart'), - ).readAsStringSync(); - expect(ackPart, contains('@support.AckInfer.jsonSerializable')); + final ackPart = File(p.join(temporary.path, 'lib', 'user.ack.dart')) + .readAsStringSync(); + expect(ackPart, contains('@support.Schemable.generatedJson')); expect(ackPart, contains('support.AckModelAdapter')); expect(ackPart, contains('final support.Role role;')); _expectSuccess(await _run(temporary, ['test']), 'dart test'); diff --git a/packages/ack_generator/test/integration/legacy_runtime_build_test.dart b/packages/ack_generator/test/integration/legacy_runtime_build_test.dart index 548486f2..65238f99 100644 --- a/packages/ack_generator/test/integration/legacy_runtime_build_test.dart +++ b/packages/ack_generator/test/integration/legacy_runtime_build_test.dart @@ -23,9 +23,8 @@ void main() { 'an unchanged Ack 1.1 source builds and reports the migration deprecation', () async { var projectRoot = Directory.current; - while (!Directory( - p.join(projectRoot.path, 'packages', 'ack_generator'), - ).existsSync()) { + while (!Directory(p.join(projectRoot.path, 'packages', 'ack_generator')) + .existsSync()) { projectRoot = projectRoot.parent; } final temporary = await Directory.systemTemp.createTemp( @@ -55,8 +54,8 @@ dependency_overrides: ack_annotations: path: ${p.join(projectRoot.path, 'packages', 'ack_annotations')} '''); - File(p.join(temporary.path, 'lib', 'legacy.dart')).writeAsStringSync( - r''' + File(p.join(temporary.path, 'lib', 'legacy.dart')) + .writeAsStringSync(r''' import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; @@ -66,11 +65,9 @@ part 'legacy.g.dart'; final userSchema = Ack.object({ 'name': Ack.string(), }).passthrough(); -''', - ); - File( - p.join(temporary.path, 'test', 'legacy_test.dart'), - ).writeAsStringSync(r''' +'''); + File(p.join(temporary.path, 'test', 'legacy_test.dart')) + .writeAsStringSync(r''' import 'package:ack_legacy_compat/legacy.dart'; import 'package:test/test.dart'; diff --git a/packages/ack_generator/test/integration/model_generation_guide_build_test.dart b/packages/ack_generator/test/integration/model_generation_guide_build_test.dart index 051e5856..d22265db 100644 --- a/packages/ack_generator/test/integration/model_generation_guide_build_test.dart +++ b/packages/ack_generator/test/integration/model_generation_guide_build_test.dart @@ -23,9 +23,8 @@ void main() { 'the model-generation guide sample builds and runs in both directions', () async { var projectRoot = Directory.current; - while (!Directory( - p.join(projectRoot.path, 'packages', 'ack_generator'), - ).existsSync()) { + while (!Directory(p.join(projectRoot.path, 'packages', 'ack_generator')) + .existsSync()) { projectRoot = projectRoot.parent; } @@ -37,9 +36,9 @@ void main() { 'typesafe-schemas.mdx', ), ).readAsStringSync(); - final sample = RegExp( - r'```dart title="lib/models.dart"\n([\s\S]*?)\n```', - ).firstMatch(guide)?.group(1); + final sample = RegExp(r'```dart title="lib/models.dart"\n([\s\S]*?)\n```') + .firstMatch(guide) + ?.group(1); expect( sample, isNotNull, @@ -56,7 +55,7 @@ void main() { name: ack_model_generation_guide publish_to: none environment: - sdk: '>=3.9.0 <4.0.0' + sdk: '>=3.13.0 <4.0.0' dependencies: ack: path: ${p.join(projectRoot.path, 'packages', 'ack')} @@ -73,12 +72,10 @@ dependency_overrides: ack_annotations: path: ${p.join(projectRoot.path, 'packages', 'ack_annotations')} '''); - File( - p.join(temporary.path, 'lib', 'models.dart'), - ).writeAsStringSync(sample!); - File( - p.join(temporary.path, 'test', 'models_test.dart'), - ).writeAsStringSync(r''' + File(p.join(temporary.path, 'lib', 'models.dart')) + .writeAsStringSync(sample!); + File(p.join(temporary.path, 'test', 'models_test.dart')) + .writeAsStringSync(r''' import 'package:ack_model_generation_guide/models.dart'; import 'package:test/test.dart'; diff --git a/packages/ack_generator/test/test_utils/test_assets.dart b/packages/ack_generator/test/test_utils/test_assets.dart index 08765113..72de579b 100644 --- a/packages/ack_generator/test/test_utils/test_assets.dart +++ b/packages/ack_generator/test/test_utils/test_assets.dart @@ -28,9 +28,14 @@ const ackAnnotationsAsset = { 'ack_annotations|lib/ack_annotations.dart': ''' library ack_annotations; -export 'src/ack_type.dart'; +export 'package:ack/annotations.dart'; ''', - 'ack_annotations|lib/src/ack_type.dart': ''' +}; + +const ackPackageAnnotationsAsset = { + 'ack|lib/annotations.dart': ''' +library ack; + import 'package:meta/meta_meta.dart'; @Target({TargetKind.topLevelVariable, TargetKind.getter}) @@ -45,6 +50,7 @@ const ackPackageAsset = { 'ack|lib/ack.dart': ''' library ack; +export 'annotations.dart'; export 'src/ack.dart'; export 'src/schemas/schema_model.dart'; export 'src/schemas/object_schema.dart'; @@ -352,5 +358,6 @@ class SchemaFailure extends SchemaResult { Map get allAssets => { ...metaAssets, ...ackAnnotationsAsset, + ...ackPackageAnnotationsAsset, ...ackPackageAsset, }; diff --git a/scripts/src/staging.dart b/scripts/src/staging.dart index 84460805..9c62acc3 100644 --- a/scripts/src/staging.dart +++ b/scripts/src/staging.dart @@ -92,20 +92,26 @@ String overrideWithStagedPackages( /// Removes the [flutterMembers] workspace entries and the Flutter SDK /// constraint from a workspace root [pubspec]. /// -/// The complete workspace cannot resolve on the minimum Dart SDK, because -/// `flutter_test` pins an older `test_api` than the analyzer floor of -/// `ack_generator` allows. Dropping the Flutter members produces a resolvable -/// pure-Dart workspace that still covers every other package. +/// [excludedMembers] removes packages with a newer SDK minimum, such as the +/// generator and its example, from the runtime minimum-SDK check. String toPureDartWorkspace( String pubspec, { required Set flutterMembers, + Set excludedMembers = const {}, }) { final editor = YamlEditor(pubspec); final members = _valueAt(editor, ['workspace']); if (members is List) { - editor.update([ - 'workspace', - ], members.where((member) => !flutterMembers.contains(member)).toList()); + editor.update( + ['workspace'], + members + .where( + (member) => + !flutterMembers.contains(member) && + !excludedMembers.contains(member), + ) + .toList(), + ); } if (_valueAt(editor, ['environment', 'flutter']) != null) { editor.remove(['environment', 'flutter']); diff --git a/scripts/stage_min_sdk_workspace.dart b/scripts/stage_min_sdk_workspace.dart index 46a45eac..3e191aca 100644 --- a/scripts/stage_min_sdk_workspace.dart +++ b/scripts/stage_min_sdk_workspace.dart @@ -9,46 +9,55 @@ import 'src/staging.dart'; /// Copies the repository into a pure-Dart workspace that resolves on the /// minimum Dart SDK. /// -/// The complete workspace cannot resolve on Dart 3.9, because `flutter_test` -/// pins `test_api 0.7.6` while the root `test` constraint and the Analyzer 10 -/// dependency of `ack_generator` require a newer `test_api`. Removing the -/// Flutter workspace members leaves a resolvable workspace that still covers -/// every pure-Dart package. The Flutter packages get their own staged lane -/// through `scripts/stage_package.dart`. +/// Removes Flutter members so Dart can resolve the staged workspace without +/// a Flutter SDK. The runtime-only mode also removes generator consumers, +/// which require a newer Dart SDK than the runtime packages. /// /// Usage: -/// `dart scripts/stage_min_sdk_workspace.dart ` +/// `dart scripts/stage_min_sdk_workspace.dart [--runtime-only]` +/// +/// `--runtime-only` excludes the generator and its example. They require +/// Dart 3.13 and have a separate minimum-SDK check. /// /// The script prints the staged workspace root on success. void main(List args) { - if (args.length != 1) { + final positional = args.where((arg) => arg != '--runtime-only').toList(); + if (positional.length != 1 || positional.single.startsWith('--')) { stderr.writeln( - 'Usage: dart scripts/stage_min_sdk_workspace.dart ', + 'Usage: dart scripts/stage_min_sdk_workspace.dart ' + '[--runtime-only]', ); exitCode = 64; return; } - final output = args.single; + final output = positional.single; stageTrackedFiles(output, paths: const ['.']); final root = File(p.join(output, 'pubspec.yaml')); final flutterMembers = _flutterWorkspaceMembers(output, root); + final excludedMembers = args.contains('--runtime-only') + ? const {'packages/ack_generator', 'example'} + : const {}; root.writeAsStringSync( toPureDartWorkspace( root.readAsStringSync(), flutterMembers: flutterMembers, + excludedMembers: excludedMembers, ), ); // Removing the sources as well keeps the staged tree consistent: every // remaining pubspec belongs to the staged workspace. - for (final member in flutterMembers) { + for (final member in {...flutterMembers, ...excludedMembers}) { Directory(p.join(output, member)).deleteSync(recursive: true); } stdout - ..writeln('Removed Flutter workspace members: ${flutterMembers.join(', ')}') + ..writeln( + 'Removed workspace members: ' + '${{...flutterMembers, ...excludedMembers}.join(', ')}', + ) ..writeln(output); } diff --git a/test/scripts/api_check_test.dart b/test/scripts/api_check_test.dart index fd029239..f738fa63 100644 --- a/test/scripts/api_check_test.dart +++ b/test/scripts/api_check_test.dart @@ -3,50 +3,46 @@ import 'dart:io'; import 'package:test/test.dart'; void main() { - test( - 'additive mode changes only the API version check mode', - () async { - final fakeBin = Directory.systemTemp.createTempSync('ack-api-mode-'); - final workingDirectory = Directory.systemTemp.createTempSync( - 'ack-api-mode-report-', - ); - addTearDown(() => fakeBin.deleteSync(recursive: true)); - addTearDown(() => workingDirectory.deleteSync(recursive: true)); - final arguments = File('${workingDirectory.path}/arguments.txt'); - _writeExecutable(fakeBin, 'dart', '''#!/bin/sh + test('additive mode changes only the API version check mode', () async { + final fakeBin = Directory.systemTemp.createTempSync('ack-api-mode-'); + final workingDirectory = Directory.systemTemp.createTempSync( + 'ack-api-mode-report-', + ); + addTearDown(() => fakeBin.deleteSync(recursive: true)); + addTearDown(() => workingDirectory.deleteSync(recursive: true)); + final arguments = File('${workingDirectory.path}/arguments.txt'); + _writeExecutable(fakeBin, 'dart', '''#!/bin/sh if [ "\$3" = "activate" ]; then exit 0; fi printf '%s\\n' "\$@" > '${arguments.path}' exit 23 '''); - final scriptPath = File('scripts/api_check.dart').absolute.path; - final environment = { - ...Platform.environment, - 'PATH': '${fakeBin.path}:/usr/bin:/bin', - }; + final scriptPath = File('scripts/api_check.dart').absolute.path; + final environment = { + ...Platform.environment, + 'PATH': '${fakeBin.path}:/usr/bin:/bin', + }; - final additive = await Process.run( - Platform.resolvedExecutable, - [scriptPath, 'ack', '1.6.2', '--allow-additive'], - workingDirectory: workingDirectory.path, - environment: environment, - ); - expect(additive.exitCode, 1); - expect(arguments.readAsStringSync(), contains('onlyBreakingChanges')); + final additive = await Process.run( + Platform.resolvedExecutable, + [scriptPath, 'ack', '1.6.2', '--allow-additive'], + workingDirectory: workingDirectory.path, + environment: environment, + ); + expect(additive.exitCode, 1); + expect(arguments.readAsStringSync(), contains('onlyBreakingChanges')); - final strict = await Process.run( - Platform.resolvedExecutable, - [scriptPath, 'ack', '1.6.2'], - workingDirectory: workingDirectory.path, - environment: environment, - ); - expect(strict.exitCode, 1); - expect( - arguments.readAsStringSync(), - isNot(contains('onlyBreakingChanges')), - ); - }, - skip: Platform.isWindows ? 'Uses POSIX test executables.' : false, - ); + final strict = await Process.run( + Platform.resolvedExecutable, + [scriptPath, 'ack', '1.6.2'], + workingDirectory: workingDirectory.path, + environment: environment, + ); + expect(strict.exitCode, 1); + expect( + arguments.readAsStringSync(), + isNot(contains('onlyBreakingChanges')), + ); + }, skip: Platform.isWindows ? 'Uses POSIX test executables.' : false); for (final baseline in ['1.1.0', '1.2.0', '1.3.0']) { test( @@ -105,38 +101,34 @@ exit 23 skip: Platform.isWindows ? 'Uses POSIX test executables.' : false, ); - test( - 'stale reports do not satisfy the current API check', - () async { - final fakeBin = Directory.systemTemp.createTempSync('ack-api-check-'); - final workingDirectory = Directory.systemTemp.createTempSync( - 'ack-api-report-', - ); - addTearDown(() => fakeBin.deleteSync(recursive: true)); - addTearDown(() => workingDirectory.deleteSync(recursive: true)); + test('stale reports do not satisfy the current API check', () async { + final fakeBin = Directory.systemTemp.createTempSync('ack-api-check-'); + final workingDirectory = Directory.systemTemp.createTempSync( + 'ack-api-report-', + ); + addTearDown(() => fakeBin.deleteSync(recursive: true)); + addTearDown(() => workingDirectory.deleteSync(recursive: true)); - _writeExecutable(fakeBin, 'dart', '#!/bin/sh\nexit 0\n'); - final staleReport = File( - '${workingDirectory.path}/api-compat-ack-vs-1.0.0.md', - )..writeAsStringSync('stale report'); - final scriptPath = File('scripts/api_check.dart').absolute.path; + _writeExecutable(fakeBin, 'dart', '#!/bin/sh\nexit 0\n'); + final staleReport = File( + '${workingDirectory.path}/api-compat-ack-vs-1.0.0.md', + )..writeAsStringSync('stale report'); + final scriptPath = File('scripts/api_check.dart').absolute.path; - final result = await Process.run( - Platform.resolvedExecutable, - [scriptPath, 'ack', '1.0.0'], - workingDirectory: workingDirectory.path, - environment: { - ...Platform.environment, - 'PATH': '${fakeBin.path}:/usr/bin:/bin', - }, - ); + final result = await Process.run( + Platform.resolvedExecutable, + [scriptPath, 'ack', '1.0.0'], + workingDirectory: workingDirectory.path, + environment: { + ...Platform.environment, + 'PATH': '${fakeBin.path}:/usr/bin:/bin', + }, + ); - expect(result.exitCode, 1); - expect(result.stderr, contains('did not create')); - expect(staleReport.existsSync(), isFalse); - }, - skip: Platform.isWindows ? 'Uses POSIX test executables.' : false, - ); + expect(result.exitCode, 1); + expect(result.stderr, contains('did not create')); + expect(staleReport.existsSync(), isFalse); + }, skip: Platform.isWindows ? 'Uses POSIX test executables.' : false); } void _writeExecutable(Directory directory, String name, String contents) { diff --git a/test/scripts/stage_package_test.dart b/test/scripts/stage_package_test.dart index 1a2c0741..5b28d19e 100644 --- a/test/scripts/stage_package_test.dart +++ b/test/scripts/stage_package_test.dart @@ -56,10 +56,9 @@ void main() { loadYaml(File('$staged/pubspec.yaml').readAsStringSync()) as Map; expect(pubspec['dependency_overrides'], { 'ack': {'path': '../ack'}, - 'ack_annotations': {'path': '../ack_annotations'}, }); - for (final sibling in const ['ack', 'ack_annotations']) { + for (final sibling in const ['ack']) { final siblingPubspec = File( '${output.path}/staged/packages/$sibling/pubspec.yaml', ); @@ -71,6 +70,10 @@ void main() { isFalse, ); } + expect( + Directory('${output.path}/staged/packages/ack_annotations').existsSync(), + isFalse, + ); }); test('an unknown package reports the usage', () { diff --git a/test/scripts/staging_test.dart b/test/scripts/staging_test.dart index abc94a4c..d45d90a8 100644 --- a/test/scripts/staging_test.dart +++ b/test/scripts/staging_test.dart @@ -49,6 +49,28 @@ dependencies: }); group('toPureDartWorkspace', () { + test('excludes generator members for the runtime SDK check', () { + final runtime = + loadYaml( + toPureDartWorkspace( + ''' +name: ack_workspace +environment: + sdk: '>=3.9.0 <4.0.0' +workspace: + - packages/ack + - packages/ack_generator + - example +''', + flutterMembers: const {}, + excludedMembers: const {'packages/ack_generator', 'example'}, + ), + ) + as Map; + expect(runtime['workspace'], ['packages/ack']); + expect((runtime['environment'] as Map)['sdk'], '>=3.9.0 <4.0.0'); + }); + test('drops the Flutter members and the Flutter SDK constraint', () { const pubspec = ''' name: ack_workspace