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
38 changes: 38 additions & 0 deletions example/lib/schema_types_transforms.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:ack/ack.dart';
import 'package:ack_annotations/ack_annotations.dart';

part 'schema_types_transforms.g.dart';

class Color {
final String value;
const Color(this.value);
}

class TagList {
final List<String> value;
const TagList(this.value);
}

final baseColorSchema = Ack.string();

@AckType()
final colorSchema = Ack.string().transform<Color>((value) => Color(value!));

@AckType()
final profileSchema = Ack.object({
'homepage': Ack.uri(),
'birthday': Ack.date(),
'lastLogin': Ack.datetime(),
'timeout': Ack.duration(),
'links': Ack.list(Ack.uri()),
'favoriteColor': Ack.string().transform<Color>((value) => Color(value!)),
'slug': Ack.string().transform<String>((value) => value! + '#'),
'accent': colorSchema,
'colors': Ack.list(colorSchema),
'customColors': Ack.list(
baseColorSchema.transform<Color>((value) => Color(value!)),
),
'tagList': Ack.list(
Ack.string(),
).transform<TagList>((value) => TagList(value!)),
});
72 changes: 72 additions & 0 deletions example/lib/schema_types_transforms.g.dart

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

Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ final class DiscriminatedObjectSchema<T extends Object> extends AckSchema<T>

@override
int get hashCode {
final mapEq = MapEquality<String, AckSchema<T>>();
const mapEq = MapEquality<String, AckSchema>();
return Object.hash(
baseFieldsHashCode,
discriminatorKey,
Expand Down
4 changes: 4 additions & 0 deletions packages/ack/lib/src/utils/discriminated_branch_utils.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import '../schemas/schema.dart';

/// Returns the underlying branch schema by unwrapping any transform layers.
///
/// Discriminated branches may be wrapped in [TransformedSchema] while still
/// being object-backed at their core.
AckSchema unwrapDiscriminatedBranchSchema(AckSchema schema) {
var current = schema;
while (current is TransformedSchema) {
Expand Down
10 changes: 10 additions & 0 deletions packages/ack/test/schemas/discriminated_object_schema_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ void main() {
final result = animalSchema.safeParse({'meow': true});
expect(result.isOk, isFalse);
});

test('fails when schemas map is empty', () {
final emptySchema = Ack.discriminated<Map<String, Object?>>(
discriminatorKey: 'type',
schemas: const <String, AckSchema<Map<String, Object?>>>{},
);

final result = emptySchema.safeParse({'type': 'cat'});
expect(result.isOk, isFalse);
});
});

group('Fluent methods', () {
Expand Down
59 changes: 47 additions & 12 deletions packages/ack_annotations/lib/src/ack_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ import 'package:meta/meta_meta.dart';
/// - `parse(data)` factory for validation + wrapping
/// - `safeParse(data)` for error handling
/// - `toJson()` for serialization
/// - `copyWith()` for immutable updates
/// - `copyWith()` for immutable updates on object wrappers whose fields can be
/// safely reparsed from their public getter values
/// - Value equality (`==`, `hashCode`)
/// - `toString()` for debugging
///
Expand All @@ -139,12 +140,13 @@ import 'package:meta/meta_meta.dart';
/// ## Collections
///
/// Lists of primitives return `List<T>`, and lists of nested schemas return
/// `List<TType>`:
/// `List<TType>`. Transformed element schemas are also supported:
/// ```dart
/// @ackType
/// final blogPostSchema = Ack.object({
/// 'tags': Ack.list(Ack.string()), // List<String>
/// 'comments': Ack.list(commentSchema), // List<CommentType>
/// 'links': Ack.list(Ack.uri()), // List<Uri>
/// });
/// ```
///
Expand All @@ -154,7 +156,7 @@ import 'package:meta/meta_meta.dart';
///
/// | Schema Type | Generated Extension Type |
/// |-------------|--------------------------|
/// | `Ack.object({...})` | `XType(Map<String, Object?>)` with field getters, copyWith, toJson |
/// | `Ack.object({...})` | `XType(Map<String, Object?>)` with field getters, conditional copyWith, toJson |
/// | `Ack.string()` | `XType(String)` implements String |
/// | `Ack.integer()` | `XType(int)` implements int |
/// | `Ack.double()` | `XType(double)` implements double |
Expand All @@ -163,15 +165,26 @@ import 'package:meta/meta_meta.dart';
/// | `Ack.literal('value')` | `XType(String)` implements String |
/// | `Ack.enumString([...])` | `XType(String)` implements String |
/// | `Ack.enumValues<T>([...])` | `XType(T)` implements T |
/// | `Ack.uri()` | `XType(Uri)` implements Uri |
/// | `Ack.date()` / `Ack.datetime()` | `XType(DateTime)` implements DateTime |
/// | `Ack.duration()` | `XType(Duration)` implements Duration |
/// | `Ack.<schema>().transform<T>(...)` | `XType(T)` implements T when `T` is explicit |
///
/// All extension types include `parse()` and `safeParse()` factory methods.
/// `toJson()` returns the validated representation value that the schema
/// produced. For transformed schemas, that means the transformed value
/// (for example `Uri`, `DateTime`, or a custom `T`), not the original wire
/// format.
///
/// ## Unsupported Schema Types
///
/// The following schema types are not currently supported for `@AckType`:
/// - **`Ack.any()`** - Not supported (defeats type safety purpose)
/// - **`Ack.anyOf()`** - Not supported (requires union types/sealed classes)
/// - **`Ack.discriminated()`** - Use @AckModel on discriminated classes instead
/// - **Transformed object schemas** - `Ack.object({...}).transform<T>()` and
/// `objectSchema.transform<T>()` are not supported
/// - **Transformed discriminated schemas** - `Ack.discriminated(...).transform<T>()`
/// and `discriminatedSchema.transform<T>()` are not supported
///
/// ## Method Chaining Support
///
Expand All @@ -180,7 +193,7 @@ import 'package:meta/meta_meta.dart';
/// - ⚠️ **`.nullable()`** - Extension type is NOT generated (see Limitations)
/// - ✅ **`.withDefault()`** - Supported, provides fallback value
/// - ✅ **`.refine()`** - Supported, adds custom validation
/// - ⚠️ **`.transform()`** - NOT recommended (changes output type, breaks extension type contract)
/// - **`.transform<T>()`** - Supported for non-object schemas when `T` is explicit
///
/// ```dart
/// @AckType()
Expand All @@ -191,26 +204,48 @@ import 'package:meta/meta_meta.dart';
/// .min(0)
/// .refine((age) => age < 150, message: 'Too old'); // ✅ Works
///
/// // @AckType()
/// // final transformed = Ack.string().transform((s) => s.length); // ❌ Don't use
/// // → Extension type would wrap String, but transform returns int
/// @AckType()
/// final transformedLink = Ack.string()
/// .transform<Uri>((value) => Uri.parse(value!)); // ✅ Works
///
/// @AckType()
/// final directUri = Ack.uri(); // ✅ Works
///
/// final validatedOnly = Ack.string().uri(); // Still represents String
/// ```
///
/// ## Limitations
///
/// - **Class annotations**: `@AckType` is not supported on classes.
/// - **Cross-file schema references**: Schema references must be in the same file
/// - ✅ Same file: `'address': addressSchema` → getter returns `AddressType`
/// - ❌ Cross-file: `'address': addressSchema` → getter returns `Map<String, Object?>`
/// - **Cross-file schema references**: Direct imports, prefixed imports, and
/// re-exported schema refs are supported.
/// - ✅ Direct import: `'address': addressSchema` → getter returns `AddressType`
/// - ✅ Prefixed import: `'address': models.addressSchema` → getter returns
/// `models.AddressType`
/// - ✅ Re-export: `'address': addressSchema` through an export works
/// - Cross-file transformed refs require the transformed representation types
/// to be visible from the consuming library.
/// - Direct-import transformed refs may fail when a representation type name
/// collides with a different visible type in the consuming library.
/// - Source-qualified transformed representation types such as `dep.Color`
/// are not supported across library boundaries.
/// - **Nullable schema variables**: Extension types are not generated for schemas
/// marked with `.nullable()` because the representation is non-nullable.
/// - Use the schema directly for nullable validation.
/// - **Object wrappers with transformed-backed fields**: `copyWith()` is not
/// generated for object wrappers, including discriminated branches, when any
/// field is backed by `Ack.uri()`, `Ack.date()`, `Ack.datetime()`,
/// `Ack.duration()`, or `.transform<T>(...)`.
/// - **List element modifiers**: List element nullability from chained
/// modifiers may not be fully inferred:
/// - ✅ `Ack.list(Ack.string())` → `List<String>`
/// - ⚠️ `Ack.list(Ack.string().nullable())` → `List<String>` (element
/// nullability lost; expected `List<String?>`)
/// - **Transform modifier**: Not supported (changes output type)
/// - **Explicit transform output required**: use `.transform<T>(...)`, not
/// `.transform(...)`, so the generator can infer the representation type.
/// - **Constraint-only string helpers stay String**: `Ack.string().uri()`,
/// `Ack.string().date()`, and `Ack.string().datetime()` validate format but do
/// not change the generated representation type.
/// - **Dart version**: Requires Dart 3.3+ for extension type support
///
/// See also: [AckModel], [AckField]
Expand Down
Loading
Loading