Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

jobs:
test:
uses: btwld/dart-actions/.github/workflows/ci.yml@main
uses: btwld/dart-actions/.github/workflows/ci.yml@9075ce1232ec77b8747953f2ff4a349190e5a805
secrets:
token: ${{ secrets.GITHUB_TOKEN }}
with:
Expand Down
20 changes: 19 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,26 @@ on:
- 'v*'

jobs:
validate-tag:
runs-on: ubuntu-latest
outputs:
semver_ok: ${{ steps.validate-tag.outputs.semver_ok }}
steps:
- name: Validate release tag as SemVer 2.0.0 with optional pre-release/build metadata
id: validate-tag
shell: bash
run: |
TAG="${{ github.ref_name }}"
if [[ ! "$TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(0|[1-9][0-9]*|[0-9A-Za-z-]+\.)*[0-9A-Za-z-]+)?(\+(0|[1-9][0-9]*|[0-9A-Za-z-]+\.)*[0-9A-Za-z-]+)?$ ]]; then
echo "::error::Release tag '$TAG' is not SemVer 2.0.0 compatible. Use tags like v1.0.0, v1.0.0-alpha.1, or v1.0.0-beta.6+exp.sha."
exit 1
fi
echo "semver_ok=true" >> "$GITHUB_OUTPUT"

publish:
uses: btwld/dart-actions/.github/workflows/publish.yml@main
needs: validate-tag
if: needs.validate-tag.outputs.semver_ok == 'true'
uses: btwld/dart-actions/.github/workflows/publish.yml@9075ce1232ec77b8747953f2ff4a349190e5a805
permissions:
id-token: write
with:
Expand Down
4 changes: 2 additions & 2 deletions docs/api-reference/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Entry point for creating schemas. See [Schema Types](../core-concepts/schemas.md
- `Ack.enumString(List<String> values)`: Creates a string schema that only accepts specific values.
- `Ack.anyOf(List<AckSchema> schemas)`: Creates an `AnyOfSchema` for union types.
- `Ack.any()`: Creates an `AnySchema` that accepts any value.
- `Ack.discriminated({required String discriminatorKey, required Map<String, AckSchema> schemas})`: Creates a discriminated union schema.
- `Ack.discriminated({required String discriminatorKey, required Map<String, AckSchema<Map<String, Object?>>> schemas})`: Creates a discriminated union schema.

## `AckSchema<T>` (Base Class)

Expand Down Expand Up @@ -285,7 +285,7 @@ Schema for union types (value must match one of several schemas).

Schema for polymorphic validation based on a discriminator field.

- Created using `Ack.discriminated({required String discriminatorKey, required Map<String, AckSchema> schemas})`
- Created using `Ack.discriminated({required String discriminatorKey, required Map<String, AckSchema<Map<String, Object?>>> schemas})`

### `AnySchema`

Expand Down
28 changes: 28 additions & 0 deletions example/test/extension_type_to_json_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:ack_example/schema_types_primitives.dart';
import 'package:ack_example/schema_types_simple.dart';
import 'package:test/test.dart';

void main() {
group('Extension type toJson', () {
test('object extension type returns map data', () {
final user = UserType.parse({'name': 'Alice', 'age': 30, 'active': true});

expect(user.toJson(), {'name': 'Alice', 'age': 30, 'active': true});
expect(user.toJson(), isA<Map<String, Object?>>());
});

test('primitive extension type returns wrapped value', () {
final password = PasswordType.parse('mySecurePassword123');

expect(password.toJson(), 'mySecurePassword123');
expect(password.toJson(), isA<String>());
});

test('collection extension type returns wrapped list', () {
final tags = TagsType.parse(['dart', 'ack']);

expect(tags.toJson(), ['dart', 'ack']);
expect(tags.toJson(), isA<List<String>>());
});
});
}
18 changes: 3 additions & 15 deletions packages/ack/lib/src/schemas/transformed_schema.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,36 +60,24 @@ class TransformedSchema<InputType extends Object, OutputType extends Object>
Object? inputValue,
SchemaContext context,
) {
// Handle TransformedSchema's own defaultValue for null input.
// Clone the default to prevent mutation of shared state.
// This must happen BEFORE delegating to wrapped schema, because the wrapped
// schema might not accept null (e.g., non-nullable StringSchema).
//
// NOTE: cloneDefault() returns List<Object?> or Map<Object?, Object?> for
// collections, which cannot be safely cast to parameterized OutputType like
// List<MyClass>. We use runtime type checking: if the clone is type-compatible,
// use it; otherwise fall back to the original (accepts mutation risk for
// parameterized collection defaults, but avoids runtime TypeError).
// Handle defaults before delegation, since wrapped schemas may reject null.
// If cloning loses generic type information, fall back to the original value.
if (inputValue == null && defaultValue != null) {
final cloned = cloneDefault(defaultValue!);
final safeDefault = (cloned is OutputType) ? cloned : defaultValue!;
return applyConstraintsAndRefinements(safeDefault, context);
}

// For non-null input OR null input without default:
// Delegate to underlying schema (handles type conversion, null validation, constraints)
// The inner schema determines if null is valid based on its own isNullable setting.
// Delegate validation/parsing to the wrapped schema for all other cases.
final originalResult = schema.parseAndValidate(inputValue, context);
if (originalResult.isFail) {
return SchemaResult.fail(originalResult.getError());
}

// Transform the validated value (may be null if underlying schema is nullable)
final validatedValue = originalResult.getOrNull();
try {
final transformedValue = transformer(validatedValue);

// Apply TransformedSchema's own constraints and refinements
return applyConstraintsAndRefinements(transformedValue, context);
} catch (e, st) {
return SchemaResult.fail(
Expand Down
1 change: 1 addition & 0 deletions packages/ack_annotations/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* **AckType**: Refined annotation parameters and improved type handling (#50).
* **AckField**: Improved field annotation correctness (#50).
* **Breaking**: `AckField.required` was replaced by `requiredMode` (`AckFieldRequiredMode.auto|required|optional`). Migrate `@AckField(required: true)` to `@AckField(requiredMode: AckFieldRequiredMode.required)` and `required: false` to `requiredMode: AckFieldRequiredMode.optional`.

## 1.0.0-beta.5 (2026-01-14)

Expand Down
4 changes: 2 additions & 2 deletions packages/ack_annotations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ Add to your `pubspec.yaml` (check [pub.dev](https://pub.dev/packages/ack_annotat

```yaml
dependencies:
ack_annotations: ^1.0.0
ack_annotations: ^1.0.0-beta.6

dev_dependencies:
ack_generator: ^1.0.0
ack_generator: ^1.0.0-beta.6
build_runner: ^2.4.0
```

Expand Down
6 changes: 3 additions & 3 deletions packages/ack_generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ Add the following dependencies to your `pubspec.yaml` (check [pub.dev](https://p

```yaml
dependencies:
ack: ^1.0.0
ack_annotations: ^1.0.0
ack: ^1.0.0-beta.6
ack_annotations: ^1.0.0-beta.6

dev_dependencies:
ack_generator: ^1.0.0
ack_generator: ^1.0.0-beta.6
build_runner: ^2.4.0
```

Expand Down
29 changes: 11 additions & 18 deletions packages/ack_generator/lib/src/analyzer/field_analyzer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,11 @@ class FieldAnalyzer {
}

// Tri-state mode is authoritative.
switch (_getRequiredMode(annotation)) {
case AckFieldRequiredMode.required:
return true;
case AckFieldRequiredMode.optional:
return false;
case AckFieldRequiredMode.auto:
return _inferRequiredFromField(field);
}
return switch (_getRequiredMode(annotation)) {
AckFieldRequiredMode.required => true,
AckFieldRequiredMode.optional => false,
AckFieldRequiredMode.auto => _inferRequiredFromField(field),
};
}

bool _inferRequiredFromField(FieldElement2 field) {
Expand All @@ -89,16 +86,12 @@ class FieldAnalyzer {
.getField('requiredMode')
?.getField('index')
?.toIntValue();
switch (modeIndex) {
case 0:
return AckFieldRequiredMode.auto;
case 1:
return AckFieldRequiredMode.required;
case 2:
return AckFieldRequiredMode.optional;
default:
return AckFieldRequiredMode.auto;
}
return switch (modeIndex) {
0 => AckFieldRequiredMode.auto,
1 => AckFieldRequiredMode.required,
2 => AckFieldRequiredMode.optional,
_ => AckFieldRequiredMode.auto,
};
}

List<ConstraintInfo> _extractConstraints(
Expand Down
Loading
Loading