Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline

* Generate a static `Model.schema` shorthand for `Model.$ack.modelSchema` on
`@AckInfer()` models.
* Add explicit `description` options to `@AckModel` and `@AckField`.
A single-line `@description` doc tag is a fallback. Untagged prose does not
become schema data (#175).
* Escape control characters in generated string literals, including inferred
pattern values (#164).

## 1.6.2

Expand Down
26 changes: 26 additions & 0 deletions docs/core-concepts/typesafe-schemas.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,32 @@ static final Profile Function(Map<String, dynamic>) fromJson =
ProfileSchema.fromJson;
```

Use `@AckModel(description: ...)` and `@AckField(description: ...)` to export
schema descriptions. A single-line `@description` tag in a `///` or `/** */`
doc comment is a fallback. The tag exports only text on its own line. Untagged
prose does not become schema data. Explicit annotation text takes precedence.
Blank or duplicate tags fail generation. `@description` is an Ack convention,
not a Dart annotation.

A class description applies to its object schema and model codec. This includes
sealed union bases and branches. A field description applies at its property.
It does not change a nested model's own description.

```dart
/// A task the person can complete.
@AckModel(description: 'A task the person can complete.')
final class Task with _$TaskAck {
const Task({required this.id, required this.title});

final String id;

/// @description What to do.
final String title;
}

TaskSchema.toJsonSchema()['description']; // 'A task the person can complete.'
```

Use `@AckModel(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
Expand Down
48 changes: 25 additions & 23 deletions example/lib/class_first_models.ack.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion example/lib/class_first_models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,39 @@ import 'package:ack_annotations/ack_annotations.dart';
part 'class_first_models.ack.dart';
part 'class_first_models.ack.g.dart';

@AckModel(caseStyle: AckCaseStyle.snake)
/// A signed-in user's account.
@AckModel(
caseStyle: AckCaseStyle.snake,
description: "A signed-in user's account.",
)
final class Account with _$AccountAck {
const Account({
required this.displayName,
this.website,
this.role = 'member',
});

/// The name shown to other users.
@MinLength(2)
@AckField(description: 'The name shown to other users.')
final String displayName;
final Uri? website;
final String role;

static final fromJson = AccountSchema.fromJson;
}

/// A pet, identified by its `type`.
/// @description A pet, identified by its `type`.
@AckModel(discriminatorKey: 'type')
sealed class Pet with _$PetAck {
const Pet({required this.id});

final String id;
}

/// A cat with a limited number of lives.
/// @description A cat with a limited number of lives.
@AckModel(discriminatorValue: 'cat')
final class Cat extends Pet with _$CatAck {
const Cat({required super.id, required this.lives});
Expand Down
18 changes: 18 additions & 0 deletions example/test/class_first_models_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ void main() {
expect(AccountSchema.wireSchema, isNotNull);
});

test('explicit descriptions and tags become schema descriptions', () {
final jsonSchema = AccountSchema.toJsonSchema();
final properties = jsonSchema['properties'] as Map<String, Object?>;

expect(jsonSchema['description'], "A signed-in user's account.");
expect(AccountSchema.schema.description, "A signed-in user's account.");
expect(
(properties['display_name'] as Map<String, Object?>)['description'],
'The name shown to other users.',
);
expect(PetSchema.schema.description, 'A pet, identified by its `type`.');
expect(
CatSchema.schema.description,
'A cat with a limited number of lives.',
);
expect(DogSchema.schema.description, isNull);
});

test('class-first sealed models dispatch through a discriminator', () {
final pet = PetSchema.parse({'type': 'cat', 'id': 'c1', 'lives': 9});

Expand Down
4 changes: 4 additions & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ classes use `@AckModel(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
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.
Deprecated `AckFieldPresence` remains available during migration. Unknown properties use
`unknownProperties: AckUnknownPropertyPolicy.<policy>` (`reject` by default).
Raw `Ack.object(..., additionalProperties: true)` validation preserves extras;
Expand Down
2 changes: 2 additions & 0 deletions packages/ack_annotations/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Added

* `@AckModel(description: ...)` and `@AckField(description: ...)` set schema
descriptions. Explicit values take precedence over `@description` doc tags.
* Constraint and field annotations can target function parameters.
* `@Url()`, `@Uuid()`, and `@Date()` are string format annotations.
* `format_annotations.dart` exports `@Uri()` and `@DateTime()` for prefixed
Expand Down
6 changes: 5 additions & 1 deletion packages/ack_annotations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ final class Account with _$AccountAck {

This generates the public `AccountSchema` facade plus validated `toJson`,
`safeToJson`, `copyWith`, equality, and `toString` implementations.
Field documentation becomes the field schema's description. Format
Use `@AckModel(description: ...)` and `@AckField(description: ...)` for schema
descriptions. A single-line `@description` documentation tag is a fallback.
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
prefix for `@formats.Uri()` and `@formats.DateTime()`. The prefix preserves
Expand Down
8 changes: 7 additions & 1 deletion packages/ack_annotations/lib/src/ack_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ final class NotNull {
///
/// [schema] must be a const tear-off of a top-level function returning an Ack
/// schema. The generator validates the declaration and follows its expression.
/// A no-op `@AckField()` is rejected.
/// [description] describes this property, not a nested model's schema.
/// It takes precedence over an `@description` doc tag. A no-op `@AckField()`
/// is rejected.
///
/// Prefer `@Optional()` and `@Required()` for key presence. [presence] is
/// deprecated and will be removed in 2.0.0.
Expand All @@ -60,6 +62,7 @@ final class AckField {
/// Creates a field annotation.
const AckField({
this.schema,
this.description,
@Deprecated(
'Use @Optional() or @Required(). AckField.presence will be removed in '
'2.0.0.',
Expand All @@ -70,6 +73,9 @@ final class AckField {
/// Top-level schema-function tear-off followed by `ack_generator`.
final Object Function()? schema;

/// Schema description for this property.
final String? description;

/// Presence override applied after constructor inference.
@Deprecated(
'Use @Optional() or @Required(). AckField.presence will be removed in 2.0.0.',
Expand Down
5 changes: 5 additions & 0 deletions packages/ack_annotations/lib/src/ack_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ final class AckModel {
/// Creates a class-first Ack model annotation.
const AckModel({
this.schemaName,
this.description,
this.caseStyle = AckCaseStyle.none,
this.discriminatorKey,
this.discriminatorValue,
Expand Down Expand Up @@ -68,6 +69,10 @@ final class AckModel {
/// `<ClassName>Schema`, preserving the model class's written capitalization.
final String? schemaName;

/// Schema description. This takes precedence over an `@description` doc tag.
/// Untagged documentation does not become a schema description.
final String? description;

/// Naming convention applied to JSON field keys.
final AckCaseStyle caseStyle;

Expand Down
11 changes: 9 additions & 2 deletions packages/ack_generator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,21 @@
lint rules and coverage reports cover only hand-written source.
* Generated constructors, parsing and JSON members, `$ack`, and class-first
facades now have doc comments.
* Schema descriptions come only from `description:` or an `@description` doc
tag. This replaces the unreleased development behavior that exported every
field doc comment. Untagged prose and `//` comments never become schema
data. Shared `AckSchemaInference.applyDescription` uses the same rule.

### Added

* `@AckInfer()` models declare `static final schema` as a shorthand for
`$ack.modelSchema`. The generator omits this member when `schema` conflicts
with a field, discriminator, or annotated declaration.
* Class-first generation uses field documentation as schema descriptions and
supports the new string format annotations.
* Class-first generation supports explicit `@AckModel` and `@AckField`
descriptions, plus single-line `@description` doc tags. Explicit annotation
values take precedence. Class descriptions apply to the wire object and
model codec; field descriptions apply only to that property.
* Class-first generation supports the new string format annotations.
* `inference.dart` exports shared type and constraint inference for other
generators. The class-first generator uses this library.

Expand Down
12 changes: 9 additions & 3 deletions packages/ack_generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,19 @@ void main() {
}
```

Field documentation becomes an Ack schema description. The generator shares
its type and constraint inference through
Use `@AckModel(description: ...)` and `@AckField(description: ...)` for schema
descriptions. A single-line `@description` tag in a `///` or `/** */` doc
comment is a fallback. The tag exports only text on its own line. Untagged
prose never becomes schema data. Duplicate or blank tags fail generation.
Annotation text takes precedence over a tag. A class description applies to
both the object schema and model codec, including union bases and branches.
A field description applies at its property without changing a nested model.
The generator shares its type and constraint inference through
`package:ack_generator/inference.dart`. A second generator can use
`AckSchemaInference.inferType`, `applyConstraints`, and `applyDescription`.
The caller resolves application model types and import prefixes. For function
parameters, read the documentation comment from the source AST and pass it as
`sourceComment` to `applyDescription`.
`sourceComment` to `applyDescription`. That method uses the same tag-only rule.

## Schema support

Expand Down
Loading
Loading