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
10 changes: 5 additions & 5 deletions example/lib/schema_types_transforms.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class TagList {
final baseColorSchema = Ack.string();

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

@AckType()
final profileSchema = Ack.object({
Expand All @@ -25,14 +25,14 @@ final profileSchema = Ack.object({
'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! + '#'),
'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!)),
baseColorSchema.transform<Color>((value) => Color(value)),
),
'tagList': Ack.list(
Ack.string(),
).transform<TagList>((value) => TagList(value!)),
).transform<TagList>((value) => TagList(value)),
});
15 changes: 11 additions & 4 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -270,20 +270,27 @@ final signupSchema = Ack.object({

### Transformations

Transform converts validated data to a different type. The transformer receives a **nullable** value (`T?`) because the underlying schema might be nullable:
Transform converts validated data to a different type. The transformer
receives a **non-null** value (`T`) after validation. If a nullable schema
accepts `null`, that `null` passes through without invoking the transformer:

```dart
// Transform string to DateTime (value is non-null after validation, so use !)
// Transform string to DateTime
final dateSchema = Ack.string()
.minLength(10)
.transform<DateTime>((value) => DateTime.parse(value!));
.transform<DateTime>((value) => DateTime.parse(value));

// Nullable inputs bypass the transformer and stay null
final optionalDateSchema = Ack.string()
.nullable()
.transform<DateTime>((value) => DateTime.parse(value));

// Add computed fields to objects
final userWithAge = Ack.object({
'name': Ack.string(),
'birthYear': Ack.integer(),
}).transform<Map<String, Object?>>((data) {
final birthYear = data!['birthYear'] as int;
final birthYear = data['birthYear'] as int;
return {...data, 'age': DateTime.now().year - birthYear};
});
```
Expand Down
8 changes: 4 additions & 4 deletions packages/ack/lib/src/ack.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ final class Ack {
static TransformedSchema<String, DateTime> date() {
return string()
.date() // Validates ISO 8601 date format (YYYY-MM-DD) first
.transform<DateTime>((s) => DateTime.parse(s!));
.transform<DateTime>((s) => DateTime.parse(s));
}

/// Creates a datetime schema that parses ISO 8601 datetime strings into DateTime objects.
Expand All @@ -98,7 +98,7 @@ final class Ack {
static TransformedSchema<String, DateTime> datetime() {
return string()
.datetime() // Validates ISO 8601 datetime format with timezone first
.transform<DateTime>((s) => DateTime.parse(s!));
.transform<DateTime>((s) => DateTime.parse(s));
}

/// Creates a schema that parses URI strings into [Uri] objects.
Expand All @@ -115,7 +115,7 @@ final class Ack {
static TransformedSchema<String, Uri> uri() {
return string()
.uri() // Validates URI format first
.transform<Uri>((s) => Uri.parse(s!));
.transform<Uri>((s) => Uri.parse(s));
}

/// Creates a schema that parses millisecond integers into [Duration] objects.
Expand All @@ -131,6 +131,6 @@ final class Ack {
/// final timeout = Ack.duration().min(Duration(minutes: 1)).max(Duration(minutes: 2));
/// ```
static TransformedSchema<int, Duration> duration() {
return integer().transform<Duration>((ms) => Duration(milliseconds: ms!));
return integer().transform<Duration>((ms) => Duration(milliseconds: ms));
}
}
4 changes: 2 additions & 2 deletions packages/ack/lib/src/schemas/discriminated_object_schema.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ Object? _serializeJsonSchemaDefaultOrNull(Object? defaultValue) {
/// 'cat': Ack.object({
/// 'type': Ack.literal('cat'),
/// 'name': Ack.string(),
/// }).transform<Animal>((map) => Cat(map!['name'] as String)),
/// }).transform<Animal>((map) => Cat(map['name'] as String)),
/// 'dog': Ack.object({
/// 'type': Ack.literal('dog'),
/// 'name': Ack.string(),
/// }).transform<Animal>((map) => Dog(map!['name'] as String)),
/// }).transform<Animal>((map) => Dog(map['name'] as String)),
/// },
/// );
/// ```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,15 @@ extension AckSchemaExtensions<T extends Object> on AckSchema<T> {

/// Transforms the validated value using the provided transformer function.
///
/// The transformer receives `T?` because the base schema may be nullable or
/// optional, meaning `null` is a valid validated value.
/// This is useful for converting data types or applying business logic transformations.
/// The [transformer] always receives a non-null `T` value. Even when this
/// schema is nullable, the transformer is only called for non-null values; if
/// the input is `null`, it passes through as `null` without invoking the
/// transformer.
///
/// This is useful for converting data types or applying business logic
/// transformations without defensively handling `null` inside the callback.
TransformedSchema<T, R> transform<R extends Object>(
R Function(T? value) transformer,
R Function(T value) transformer,
) {
return TransformedSchema(
this,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,18 +142,18 @@ extension StringSchemaExtensions on StringSchema {
/// Trims leading and trailing whitespace from the string before validation.
/// Returns a transformed schema that applies String.trim() to the input.
TransformedSchema<String, String> trim() {
return transform((s) => s?.trim() ?? '');
return transform((s) => s.trim());
}

/// Converts the string to lowercase after validation.
/// Returns a transformed schema that applies String.toLowerCase() to the input.
TransformedSchema<String, String> toLowerCase() {
return transform((s) => s?.toLowerCase() ?? '');
return transform((s) => s.toLowerCase());
}

/// Converts the string to uppercase after validation.
/// Returns a transformed schema that applies String.toUpperCase() to the input.
TransformedSchema<String, String> toUpperCase() {
return transform((s) => s?.toUpperCase() ?? '');
return transform((s) => s.toUpperCase());
}
}
19 changes: 16 additions & 3 deletions packages/ack/lib/src/schemas/transformed_schema.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ part of 'schema.dart';
/// // Parse ISO date strings into DateTime objects
/// final dateSchema = Ack.string()
/// .datetime()
/// .transform<DateTime>((s) => DateTime.parse(s!));
/// .transform<DateTime>((s) => DateTime.parse(s));
/// ```
///
/// ## Default Values
Expand All @@ -21,7 +21,7 @@ part of 'schema.dart';
/// ```dart
/// // This works - primitive defaults are safely cloned
/// final schema = Ack.string()
/// .transform((v) => v ?? 'fallback')
/// .transform((v) => v)
/// .copyWith(defaultValue: 'hello');
///
/// // Limitation: List<String> defaults may not be cloned (mutation risk)
Expand All @@ -35,7 +35,7 @@ part of 'schema.dart';
class TransformedSchema<InputType extends Object, OutputType extends Object>
extends AckSchema<OutputType> {
final AckSchema<InputType> schema;
final OutputType Function(InputType?) transformer;
final OutputType Function(InputType) transformer;

TransformedSchema(
this.schema,
Expand Down Expand Up @@ -75,6 +75,19 @@ class TransformedSchema<InputType extends Object, OutputType extends Object>
}

final validatedValue = originalResult.getOrNull();

// Null passes through without hitting the transformer when this schema
// itself is nullable. Outer nullability still applies even if the wrapped
// schema can accept null. Constraints and refinements on the transformed
// schema are typed OutputType (extends Object) and cannot accept null,
// so they must be skipped here.
if (validatedValue == null) {
if (!isNullable) {
return failNonNullable(context);
}
return SchemaResult.ok(null);
}

try {
final transformedValue = transformer(validatedValue);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ void main() {
schemas: {
'cat': Ack.object({
'name': Ack.string(),
}).transform<String>((map) => map!['name'] as String),
}).transform<String>((map) => map['name'] as String),
'dog': Ack.object({
'name': Ack.string(),
}).transform<String>((map) => map!['name'] as String),
}).transform<String>((map) => map['name'] as String),
},
);

Expand All @@ -115,7 +115,7 @@ void main() {
discriminatorKey: 'type',
schemas: {
'cat': Ack.object({'name': Ack.string()})
.transform<String>((map) => map!['name'] as String)
.transform<String>((map) => map['name'] as String)
.copyWith(description: 'cat branch'),
},
);
Expand Down
4 changes: 2 additions & 2 deletions packages/ack/test/documentation/advanced_features_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ void main() {
final dateSchema = Ack.string()
.matches(r'^\d{4}-\d{2}-\d{2}$')
.transform<DateTime>((dateStr) {
return DateTime.parse(dateStr!);
return DateTime.parse(dateStr);
});

final result = dateSchema.safeParse('2024-01-15');
Expand All @@ -119,7 +119,7 @@ void main() {
.matches(r'^[\d\s\-\(\)\+]+$')
.transform((phone) {
// Remove all non-digit characters except +
return phone!.replaceAll(RegExp(r'[^\d\+]'), '');
return phone.replaceAll(RegExp(r'[^\d\+]'), '');
});

final result = phoneSchema.safeParse('+1 (555) 123-4567');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ void main() {

test('transform examples adjust output data', () {
final upperSchema = Ack.string().transform(
(s) => s?.toUpperCase() ?? '',
(s) => s.toUpperCase(),
);
expect(upperSchema.safeParse('hello').getOrThrow(), equals('HELLO'));

Expand All @@ -410,7 +410,7 @@ void main() {
'name': Ack.string(),
'birthYear': Ack.integer(),
}).transform((data) {
final birthYear = data!['birthYear'] as int;
final birthYear = data['birthYear'] as int;
final age = DateTime.now().year - birthYear;
return {...data, 'age': age};
});
Expand All @@ -425,7 +425,7 @@ void main() {

final dateSchema = Ack.string()
.matches(r'^\d{4}-\d{2}-\d{2}$')
.transform<DateTime>((s) => DateTime.parse(s!));
.transform<DateTime>((s) => DateTime.parse(s));
final parsedDate =
dateSchema.safeParse('2024-01-01').getOrThrow() as DateTime;
expect(parsedDate.year, equals(2024));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ void main() {
'name': Ack.string(),
'birthYear': Ack.integer(),
}).transform((data) {
final birthYear = data!['birthYear'] as int;
final birthYear = data['birthYear'] as int;
final age = DateTime.now().year - birthYear;
return {...data, 'age': age};
});
Expand Down
4 changes: 2 additions & 2 deletions packages/ack/test/documentation/social_media_demos_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ void main() {
'name': Ack.string(),
'birthYear': Ack.integer().min(1900).max(2024),
}).transform((data) {
final age = DateTime.now().year - (data!['birthYear'] as int);
final age = DateTime.now().year - (data['birthYear'] as int);
return {...data, 'age': age};
});

Expand All @@ -29,7 +29,7 @@ void main() {
'name': Ack.string(),
'birthYear': Ack.integer().min(1900).max(2024),
}).transform((data) {
final age = DateTime.now().year - (data!['birthYear'] as int);
final age = DateTime.now().year - (data['birthYear'] as int);
return {...data, 'age': age};
});

Expand Down
8 changes: 4 additions & 4 deletions packages/ack/test/integration/complex_schema_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ void main() {
'quantity': Ack.integer().positive(),
}).transform<Map<String, Object?>>((product) {
return {
...product!,
...product,
'total':
(product['price'] as double) * (product['quantity'] as int),
};
Expand Down Expand Up @@ -66,7 +66,7 @@ void main() {
}, message: 'Order total must be positive')
.transform<Map<String, Object?>>((order) {
// Calculate order summary
final items = order!['items'] as List;
final items = order['items'] as List;
final subtotal = items.fold<double>(
0,
(sum, item) => sum + (item as Map)['total'],
Expand Down Expand Up @@ -141,7 +141,7 @@ void main() {
.partial() // Make all fields optional
.transform<Map<String, Object?>>((obj) {
// Handle missing data gracefully
final data = obj!['data'] as Map<String, Object?>?;
final data = obj['data'] as Map<String, Object?>?;
return {
'hasData': data != null,
'value': data?['value'] ?? 'default',
Expand Down Expand Up @@ -182,7 +182,7 @@ void main() {
'profile': Ack.object({'age': Ack.integer().min(18)}),
}),
},
).transform<Map<String, Object?>>((data) => data!).refine((data) {
).transform<Map<String, Object?>>((data) => data).refine((data) {
final profile = data['profile'] as Map;
return (profile['age'] as int) < 100;
}, message: 'Age too high');
Expand Down
Loading
Loading