From afe500f69cf173c97cbcfe8ba99299cea484e1c8 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 26 Jun 2026 10:27:37 -0400 Subject: [PATCH 1/4] refactor(ack)!: remove deprecated APIs ahead of 1.0 BREAKING CHANGE: removes members that were deprecated in earlier betas. - Remove StringSchema.enumString() and .literal() instance methods; use the Ack.enumString() / Ack.literal() factories. - Remove the MapValue typedef; use JsonMap. - Mark CodecSchema.inputSchema/outputSchema and wrapPropertyConversion @internal so they leave the public surface. --- packages/ack/lib/src/common_types.dart | 3 -- .../ack/lib/src/constraints/validators.dart | 18 ++++---- packages/ack/lib/src/json_schema.dart | 1 - .../src/json_schema/json_schema_utils.dart | 3 ++ .../ack/lib/src/schemas/codec_schema.dart | 6 ++- .../extensions/string_schema_extensions.dart | 14 ------- packages/ack/lib/src/utils/default_utils.dart | 2 +- packages/ack/test/default_utils_test.dart | 2 +- .../string_schema_extensions_test.dart | 42 +------------------ packages/ack_generator/analysis_options.yaml | 4 +- 10 files changed, 24 insertions(+), 71 deletions(-) diff --git a/packages/ack/lib/src/common_types.dart b/packages/ack/lib/src/common_types.dart index a684cb5c..618d58ae 100644 --- a/packages/ack/lib/src/common_types.dart +++ b/packages/ack/lib/src/common_types.dart @@ -2,6 +2,3 @@ library; /// Canonical JSON map type used for object boundary/runtime values. typedef JsonMap = Map; - -/// Backwards-compatible alias for [JsonMap]. -typedef MapValue = JsonMap; diff --git a/packages/ack/lib/src/constraints/validators.dart b/packages/ack/lib/src/constraints/validators.dart index d4e6c8a4..cdf66efa 100644 --- a/packages/ack/lib/src/constraints/validators.dart +++ b/packages/ack/lib/src/constraints/validators.dart @@ -43,7 +43,7 @@ class InvalidTypeConstraint extends Constraint if (t == int) return value is int; if (t == double) return value is double; if (t == bool) return value is bool; - if (t == MapValue || t == Map) return value is Map; + if (t == JsonMap || t == Map) return value is Map; if (t == List) return value is List; // Conservative fallback for other types @@ -88,8 +88,8 @@ class InvalidTypeConstraint extends Constraint /// Placeholder: Constraint for when an object has properties not defined in its schema /// and `allowAdditionalProperties` is false. -class ObjectNoAdditionalPropertiesConstraint extends Constraint - with Validator { +class ObjectNoAdditionalPropertiesConstraint extends Constraint + with Validator { final String unexpectedPropertyKey; ObjectNoAdditionalPropertiesConstraint({required this.unexpectedPropertyKey}) : super( @@ -99,14 +99,14 @@ class ObjectNoAdditionalPropertiesConstraint extends Constraint ); @override - bool isValid(MapValue value) { + bool isValid(JsonMap value) { // This logic is handled in ObjectSchema, so this validation is conceptual. // We return false to ensure an error is always generated when this is used. return false; } @override - String buildMessage(MapValue value) { + String buildMessage(JsonMap value) { return 'Unexpected property found: "$unexpectedPropertyKey".'; } @@ -132,8 +132,8 @@ class ObjectNoAdditionalPropertiesConstraint extends Constraint /// Placeholder: Constraint for when an object is missing a required property. /// Logic is in ObjectSchema. -class ObjectRequiredPropertiesConstraint extends Constraint - with Validator { +class ObjectRequiredPropertiesConstraint extends Constraint + with Validator { final String missingPropertyKey; ObjectRequiredPropertiesConstraint({required this.missingPropertyKey}) : super( @@ -142,12 +142,12 @@ class ObjectRequiredPropertiesConstraint extends Constraint ); @override - bool isValid(MapValue value) { + bool isValid(JsonMap value) { return value.containsKey(missingPropertyKey); } @override - String buildMessage(MapValue value) { + String buildMessage(JsonMap value) { return 'Required property "$missingPropertyKey" is missing.'; } diff --git a/packages/ack/lib/src/json_schema.dart b/packages/ack/lib/src/json_schema.dart index 245d88c1..c7c155b9 100644 --- a/packages/ack/lib/src/json_schema.dart +++ b/packages/ack/lib/src/json_schema.dart @@ -4,7 +4,6 @@ /// renderer for that model. library; -export 'json_schema/json_schema_utils.dart'; export 'schema_model/ack_schema_model.dart'; export 'schema_model/ack_schema_model_builder.dart'; export 'schema_model/ack_schema_model_warning.dart'; diff --git a/packages/ack/lib/src/json_schema/json_schema_utils.dart b/packages/ack/lib/src/json_schema/json_schema_utils.dart index 3f3a73b4..2d8bd45c 100644 --- a/packages/ack/lib/src/json_schema/json_schema_utils.dart +++ b/packages/ack/lib/src/json_schema/json_schema_utils.dart @@ -1,10 +1,13 @@ /// Shared utilities for JSON Schema conversion. library; +import 'package:meta/meta.dart'; + /// Wraps property conversion with enhanced error context. /// /// When converting schema properties, this wrapper catches errors and re-throws /// them with additional context about which property caused the failure. +@internal T wrapPropertyConversion(String key, T Function() fn) { try { return fn(); diff --git a/packages/ack/lib/src/schemas/codec_schema.dart b/packages/ack/lib/src/schemas/codec_schema.dart index 8bdb7028..94592308 100644 --- a/packages/ack/lib/src/schemas/codec_schema.dart +++ b/packages/ack/lib/src/schemas/codec_schema.dart @@ -12,10 +12,14 @@ final class CodecSchema with FluentSchema>, WrapperSchema> { + /// The boundary input schema. Internal codec plumbing; not part of the + /// public API surface. + @internal final AckSchema inputSchema; /// The output schema applied to the runtime value after decoding and before - /// encoding. + /// encoding. Internal codec plumbing; not part of the public API surface. + @internal final AckSchema outputSchema; final Runtime Function(Object value) _decoder; diff --git a/packages/ack/lib/src/schemas/extensions/string_schema_extensions.dart b/packages/ack/lib/src/schemas/extensions/string_schema_extensions.dart index 6206abe5..9a232fac 100644 --- a/packages/ack/lib/src/schemas/extensions/string_schema_extensions.dart +++ b/packages/ack/lib/src/schemas/extensions/string_schema_extensions.dart @@ -1,7 +1,6 @@ import '../../constraints/comparison_constraint.dart'; import '../../constraints/pattern_constraint.dart'; import '../../constraints/string_ip_constraint.dart'; -import '../../constraints/string_literal_constraint.dart'; import '../schema.dart'; import 'ack_schema_extensions.dart'; @@ -125,19 +124,6 @@ extension StringSchemaExtensions on StringSchema { /// Adds a constraint that the string must be a valid IPv6 address. StringSchema ipv6() => ip(version: 6); - /// Adds a constraint that the string must be one of the allowed values. - @Deprecated('Use Ack.enumString() instead.') - StringSchema enumString(List allowedValues) { - return withConstraint(PatternConstraint.enumString(allowedValues)); - } - - /// Adds a constraint that the string must be exactly equal to [value]. - /// Similar to Zod's `z.literal("value")`. - @Deprecated('Use Ack.literal() instead.') - StringSchema literal(String value) { - return withConstraint(StringLiteralConstraint(value)); - } - /// Trims leading and trailing whitespace from the string before validation. /// Returns a one-way codec that applies String.trim() to the input. CodecSchema trim() { diff --git a/packages/ack/lib/src/utils/default_utils.dart b/packages/ack/lib/src/utils/default_utils.dart index 39bebd0f..15382ced 100644 --- a/packages/ack/lib/src/utils/default_utils.dart +++ b/packages/ack/lib/src/utils/default_utils.dart @@ -21,7 +21,7 @@ Object? cloneDefault(Object? value) { // Handle any Map (not just Map) so we don't skip cloned defaults // for literals inferred as Map or similar. if (value is Map) { - // Preserve String-keyed maps to keep compatibility with MapValue casts. + // Preserve String-keyed maps for JsonMap casts. if (value.keys.every((key) => key is String)) { final cloned = {}; value.forEach((key, entryValue) { diff --git a/packages/ack/test/default_utils_test.dart b/packages/ack/test/default_utils_test.dart index 816c83cd..a9fe182c 100644 --- a/packages/ack/test/default_utils_test.dart +++ b/packages/ack/test/default_utils_test.dart @@ -3,7 +3,7 @@ import 'package:test/test.dart'; void main() { group('cloneDefault', () { - test('clones Map literals into an unmodifiable MapValue', () { + test('clones Map literals into an unmodifiable JsonMap', () { final original = {'count': 1}; final cloned = cloneDefault(original) as Map; diff --git a/packages/ack/test/schemas/extensions/string_schema_extensions_test.dart b/packages/ack/test/schemas/extensions/string_schema_extensions_test.dart index 0c7e8975..74900339 100644 --- a/packages/ack/test/schemas/extensions/string_schema_extensions_test.dart +++ b/packages/ack/test/schemas/extensions/string_schema_extensions_test.dart @@ -151,46 +151,8 @@ void main() { expect(result3.isOk, isTrue); }); - group('literal', () { - test('should pass for exact string match', () { - // ignore: deprecated_member_use_from_same_package - final schema = Ack.string().literal('hello'); - - expect(schema.safeParse('hello').isOk, isTrue); - expect(schema.safeParse('hello').getOrNull(), equals('hello')); - }); - - test('should fail for different string', () { - // ignore: deprecated_member_use_from_same_package - final schema = Ack.string().literal('hello'); - - final result = schema.safeParse('world'); - expect(result.isOk, isFalse); - final error = result.getError() as SchemaConstraintsError; - expect( - error.constraints.first.message, - equals('Must be exactly "hello", but got "world".'), - ); - }); - - test('should work with empty string', () { - // ignore: deprecated_member_use_from_same_package - final schema = Ack.string().literal(''); - - expect(schema.safeParse('').isOk, isTrue); - expect(schema.safeParse('not empty').isOk, isFalse); - }); - - test('should work chained with other constraints', () { - // ignore: deprecated_member_use_from_same_package - final schema = Ack.string().minLength(3).literal('hello'); - - expect(schema.safeParse('hello').isOk, isTrue); - expect(schema.safeParse('hi').isOk, isFalse); // too short - expect(schema.safeParse('world').isOk, isFalse); // wrong literal - }); - - test('should work with Ack.literal() factory method (like Zod)', () { + group('Ack.literal() factory', () { + test('matches the exact value', () { final schema = Ack.literal('hello'); expect(schema.safeParse('hello').isOk, isTrue); diff --git a/packages/ack_generator/analysis_options.yaml b/packages/ack_generator/analysis_options.yaml index 9f880355..f7f5cba5 100644 --- a/packages/ack_generator/analysis_options.yaml +++ b/packages/ack_generator/analysis_options.yaml @@ -4,7 +4,9 @@ include: ../../analysis_options.yaml analyzer: errors: - # Suppress deprecated analyzer API warnings while we maintain compatibility. + # Suppresses deprecations from the EXTERNAL `analyzer` package (mid-migration + # from the `Element2` API back to `Element`). Not related to Ack's own API, + # which has no deprecations. Remove once ack_generator migrates off Element2. deprecated_member_use: ignore linter: From 7d46639d05953c3ae5b9d030a8f2a187038189b3 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 26 Jun 2026 10:27:56 -0400 Subject: [PATCH 2/4] docs: prepare documentation site and examples for 1.0 - Rework the site for developer experience: task-first Overview and Quickstart, a warmer narrative voice, and a learning-ordered sidebar (Essentials / How-To / Advanced / Package Authors). - Add a Codecs guide and a tested codecs example. - Validate snippets against the implementation and fix inaccuracies (string length(), SchemaResult methods, SchemaEncodeError, @AckType(name:), constrain/Validator, build flags, error.path). - Cut cross-page duplication; add code-generation and codecs coverage to the README and llms.txt. --- README.md | 118 +++++++++--- docs.json | 40 +++- docs/api-reference/index.mdx | 181 ++++++++--------- docs/core-concepts/codecs.mdx | 111 +++++++++++ docs/core-concepts/configuration.mdx | 69 +++---- docs/core-concepts/error-handling.mdx | 93 ++++----- docs/core-concepts/json-serialization.mdx | 80 ++------ docs/core-concepts/schemas.mdx | 100 ++++------ docs/core-concepts/typesafe-schemas.mdx | 74 +++---- docs/core-concepts/validation.mdx | 99 +++++----- docs/getting-started/installation.mdx | 30 ++- docs/getting-started/quickstart-tutorial.mdx | 182 +++++------------- docs/guides/common-recipes.mdx | 24 +-- .../creating-schema-converter-packages.md | 7 + docs/guides/custom-validation.mdx | 33 ++-- docs/guides/flutter-form-validation.mdx | 38 +--- docs/guides/json-schema-integration.mdx | 70 +++---- docs/guides/schema-converter-quickstart.md | 3 + docs/index.mdx | 116 ++++------- example/README.md | 1 + example/lib/codecs_example.dart | 36 ++++ example/pubspec.yaml | 6 +- example/test/codecs_example_test.dart | 37 ++++ llms.txt | 14 ++ 24 files changed, 799 insertions(+), 763 deletions(-) create mode 100644 docs/core-concepts/codecs.mdx create mode 100644 example/lib/codecs_example.dart create mode 100644 example/test/codecs_example_test.dart diff --git a/README.md b/README.md index 23006a05..801f22c1 100644 --- a/README.md +++ b/README.md @@ -5,31 +5,31 @@ [![pub package](https://img.shields.io/pub/v/ack.svg)](https://pub.dev/packages/ack) [![llms.txt](https://img.shields.io/badge/llms.txt-available-8A2BE2)](https://docs.page/btwld/ack/llms.txt) -Ack is a schema validation library for Dart and Flutter that helps you validate data with a simple, fluent API. Ack is short for "acknowledge". +Ack is a schema validation library for Dart and Flutter. It validates data with a fluent API. Ack is short for "acknowledge". For AI agents: start at [`/llms.txt`](https://docs.page/btwld/ack/llms.txt). -## Why Use Ack? +## Why use Ack? -- **Simplify Validation**: Easily handle complex data validation logic -- **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 -- **Reduce Boilerplate**: Minimize repetitive code for validation and JSON conversion -- **Type Safety**: Generate typed wrappers for hand-written Ack schemas with `@AckType()` +- **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 typed wrappers for hand-written Ack schemas with `@AckType()` ## Packages This repository is a monorepo containing: -- **[ack](./packages/ack)**: Core validation library with fluent schema building API -- **[ack_generator](./packages/ack_generator)**: Code generator for `@AckType()` extension-type wrappers -- **[ack_firebase_ai](./packages/ack_firebase_ai)**: Firebase AI (Gemini) schema converter for structured output generation +- **[ack](./packages/ack)**: Core validation library with a fluent schema-building API, codecs, and JSON Schema export +- **[ack_annotations](./packages/ack_annotations)**: The `@AckType()` annotation that marks schemas for code generation +- **[ack_generator](./packages/ack_generator)**: Code generator that turns `@AckType()` schemas into type-safe extension types +- **[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 - **[example](./example)**: Example projects demonstrating usage of all packages -## Quick Start +## Quick start -### Core Library (ack) +### Core library (ack) Add Ack to your project: @@ -42,21 +42,18 @@ Define and use a schema: ```dart import 'package:ack/ack.dart'; -// Define a schema for a user object final userSchema = Ack.object({ 'name': Ack.string().minLength(2).maxLength(50), 'email': Ack.string().email(), 'age': Ack.integer().min(0).max(120).optional(), }); -// Validate data against the schema final result = userSchema.safeParse({ 'name': 'John Doe', 'email': 'john@example.com', 'age': 30 }); -// Check if validation passed if (result.isOk) { final validData = result.getOrThrow(); print('Valid user: $validData'); @@ -68,9 +65,9 @@ if (result.isOk) { Use `.optional()` when a field may be omitted entirely. Chain `.nullable()` if a present field may hold `null`, or combine both for an optional-and-nullable value. -### Advanced Usage +### Advanced usage -For more complex validation scenarios: +For complex validation: ```dart import 'package:ack/ack.dart'; @@ -114,11 +111,71 @@ if (result.isOk) { } ``` +## Code generation + +Generate type-safe wrappers for hand-written schemas with `@AckType()`. Add +`ack_annotations` 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.g.dart'; + +@AckType() +final userSchema = Ack.object({ + 'name': Ack.string().minLength(2), + 'email': Ack.string().email(), +}); +``` + +Run the generator: + +```bash +dart run build_runner build --delete-conflicting-outputs +``` + +This emits a `UserType` extension type with `parse`/`safeParse` and typed +getters — no manual casting: + +```dart +final user = UserType.parse({'name': 'Alice', 'email': 'alice@example.com'}); +print(user.name); // typed String getter +``` + +`@AckType()` supports objects, primitives, lists, enums, explicit transforms, and discriminated unions. See the [TypeSafe Schemas guide](https://docs.page/btwld/ack/core-concepts/typesafe-schemas). + +## Codecs + +Codecs decode boundary values (the JSON you receive) into rich Dart runtime types and encode them back. Ack ships built-in codecs and lets you define your own: + +```dart +// Built-in codec: ISO 8601 String boundary <-> UTC DateTime runtime +final when = Ack.datetime(); +final dt = when.parse('2026-01-01T00:00:00Z'); // DateTime +final iso = when.encode(dt); // back to an ISO 8601 String + +// Other built-ins: Ack.date(), Ack.uri(), Ack.duration(), Ack.enumCodec(...) + +// Custom bidirectional codec +final csv = Ack.codec>( + input: Ack.string(), + decode: (s) => s.split(','), + encode: (list) => list.join(','), +); + +csv.parse('a,b,c'); // ['a', 'b', 'c'] +csv.encode(['a', 'b', 'c']); // 'a,b,c' +``` + +Use `.transform(...)` for one-way (parse-only) conversions. See the +[Codecs guide](https://docs.page/btwld/ack/core-concepts/codecs). + ## Documentation -Documentation endpoints: - Human docs: [docs.page/btwld/ack](https://docs.page/btwld/ack) -- AI agent index (stable URL): [docs.page/btwld/ack/llms.txt](https://docs.page/btwld/ack/llms.txt) +- AI agent index: [docs.page/btwld/ack/llms.txt](https://docs.page/btwld/ack/llms.txt) - Canonical plaintext source: [raw.githubusercontent.com/btwld/ack/main/llms.txt](https://raw.githubusercontent.com/btwld/ack/main/llms.txt) ## Development @@ -135,7 +192,7 @@ dart pub global activate melos melos bootstrap ``` -### Common Commands (run from root) +### Common commands (run from root) ```bash # Run tests across all packages @@ -166,35 +223,32 @@ melos version melos run publish ``` -### Development Tools - -The project includes additional development tools for maintainers: +### Development tools ```bash -# JSON Schema validation (ensures compatibility with JSON Schema Draft-7) +# JSON Schema validation (JSON Schema Draft-7 compatibility) melos validate-jsonschema -# API compatibility checking using Dart script (for semantic versioning) +# API compatibility check (for semantic versioning) melos api-check v0.2.0 # See all available scripts melos list-scripts ``` -> **Note**: Additional development documentation is available in the `tools/` directory for project maintainers. +Additional development documentation is available in the `tools/` directory. -## Versioning and Publishing +## Versioning and publishing -This project uses GitHub Releases to manage versioning and publishing. For detailed instructions on how to create releases and publish packages, see [PUBLISHING.md](./PUBLISHING.md). +This project uses GitHub Releases to manage versioning and publishing. See [PUBLISHING.md](./PUBLISHING.md) for instructions. ## Contributing -Contributions are welcome! A detailed CONTRIBUTING.md file will be added soon with specific guidelines. +Contributions are welcome. Follow these steps: -In the meantime, please follow these basic steps: 1. Fork the repository 2. Create a feature branch 3. Add your changes 4. Run tests with `melos test` -5. Make sure to follow [Conventional Commits](https://www.conventionalcommits.org/) in your commit messages +5. Follow [Conventional Commits](https://www.conventionalcommits.org/) in your commit messages 6. Submit a pull request diff --git a/docs.json b/docs.json index f3b0bebc..7ec9d74b 100644 --- a/docs.json +++ b/docs.json @@ -31,33 +31,53 @@ ] }, { - "group": "Core Concepts", + "group": "Essentials", "pages": [ { "title": "Schema Types", "href": "/core-concepts/schemas" }, { "title": "Validation Rules", "href": "/core-concepts/validation" }, - { - "title": "TypeSafe Schemas", - "href": "/core-concepts/typesafe-schemas" - }, { "title": "Error Handling", "href": "/core-concepts/error-handling" }, { "title": "JSON Serialization", "href": "/core-concepts/json-serialization" + } + ] + }, + { + "group": "How-To Guides", + "pages": [ + { + "title": "Flutter Form Validation", + "href": "/guides/flutter-form-validation" }, - { "title": "Configuration", "href": "/core-concepts/configuration" } + { "title": "Common Recipes", "href": "/guides/common-recipes" }, + { "title": "Custom Validation", "href": "/guides/custom-validation" } ] }, { - "group": "Guides", + "group": "Advanced", "pages": [ - { "title": "Custom Validation", "href": "/guides/custom-validation" }, + { "title": "Codecs", "href": "/core-concepts/codecs" }, + { + "title": "TypeSafe Schemas", + "href": "/core-concepts/typesafe-schemas" + }, { "title": "JSON Schema Integration", "href": "/guides/json-schema-integration" }, + { "title": "Configuration", "href": "/core-concepts/configuration" } + ] + }, + { + "group": "For Package Authors", + "pages": [ { - "title": "Flutter Form Validation", - "href": "/guides/flutter-form-validation" + "title": "Schema Converter Quickstart", + "href": "/guides/schema-converter-quickstart" + }, + { + "title": "Creating Schema Converters", + "href": "/guides/creating-schema-converter-packages" } ] }, diff --git a/docs/api-reference/index.mdx b/docs/api-reference/index.mdx index 90338520..22d3f6d0 100644 --- a/docs/api-reference/index.mdx +++ b/docs/api-reference/index.mdx @@ -2,9 +2,7 @@ title: API Reference --- -This page provides a quick reference for the core Ack classes, methods, and -annotations. For detailed explanations and usage examples, refer to the -specific guides linked below. +Quick reference for the core Ack classes, methods, and annotations. For detailed explanations and examples, see the specific guides linked below. ## Core `Ack` Class @@ -30,6 +28,11 @@ Entry point for creating schemas. See [Schema Types](../core-concepts/schemas.md - `Ack.anyOf(List schemas)`: Creates an `AnyOfSchema` for union types. - `Ack.any()`: Creates an `AnySchema` that accepts any non-null JSON-safe value. Chain `.nullable()` to allow `null`. +- `Ack.instance()`: Creates a schema that checks a value is a + Dart instance of `T` (runtime type check; handy as a codec `output` schema). +- `Ack.codec(...)` / `Ack.date()` / `Ack.datetime()` / `Ack.uri()` / + `Ack.duration()` / `Ack.enumCodec(...)`: Create codecs. See + [Codecs](../core-concepts/codecs.mdx). - `Ack.lazy(String name, AckSchema Function() builder)`: Creates a memoized deferred schema reference for recursive schema graphs. JSON Schema export renders Draft-7 `definitions` / `$ref` entries using `name`. @@ -46,33 +49,32 @@ Base class for all schema types. ### Primary Validation Methods -- `SchemaResult safeParse(Object? data, {String? debugName})`: Validates the input data and returns a result. Never throws exceptions - returns `SchemaResult` with either success or failure. Use `safeParse(...).getOrNull()` to obtain the validated value with no exception. -- `Runtime? parse(Object? data, {String? debugName})`: Validates the input data and returns the value. Throws `AckException` if validation fails. -- `SchemaResult safeParseAs(Object? data, TOut Function(Runtime?) map, {String? debugName})`: Parses and maps the validated value to another type. +- `SchemaResult safeParse(Object? data, {String? debugName})`: Validates `data` and returns a `SchemaResult`. Never throws; use `.getOrNull()` to get the value without risk of an exception. +- `Runtime? parse(Object? data, {String? debugName})`: Validates `data` and returns the value; throws `AckException` on failure. +- `SchemaResult safeParseAs(Object? data, TOut Function(Runtime?) map, {String? debugName})`: Parses and maps the validated value to `TOut`. - `TOut parseAs(Object? data, TOut Function(Runtime?) map, {String? debugName})`: Throwing variant of `safeParseAs`. -- `SchemaResult safeEncode(Runtime? value, {String? debugName})`: Encodes a runtime value back to the boundary representation. +- `SchemaResult safeEncode(Runtime? value, {String? debugName})`: Encodes a runtime value to the boundary representation. - `Boundary? encode(Runtime? value, {String? debugName})`: Throwing variant of `safeEncode`. ### Schema Modification Methods -- `AckSchema nullable({bool value = true})`: Returns a new schema that also accepts `null` values. +- `AckSchema nullable({bool value = true})`: Returns a new schema that also accepts `null`. - `AckSchema optional({bool value = true})`: Returns a new schema marked as optional (for object fields). -- `AckSchema describe(String description)`: Adds a description for documentation and JSON Schema generation. +- `AckSchema describe(String description)`: Attaches a description for documentation and JSON Schema generation. - `DefaultSchema withDefault(Runtime value)`: Wraps the schema in a `DefaultSchema` that supplies `value` when the parse input is `null`. Primitive schemas (`StringSchema`, `IntegerSchema`, `DoubleSchema`, `NumberSchema`, `BooleanSchema`) are strict — they reject values whose Dart runtime type doesn't match. `IntegerSchema` and `DoubleSchema` do not overlap (`42.0` fails `Ack.integer()`, `42` fails `Ack.double()`); use `Ack.number()` when either is acceptable. For non-`num` boundary types (e.g. numeric strings), use [`transform`](../core-concepts/schemas.mdx#transformations) or [`codec`](#codecschemaboundary-runtime) to convert before validation. ### Custom Validation Methods -- `AckSchema constrain(Constraint constraint, {String? message})`: Applies a custom validation constraint. -- `AckSchema withConstraint(Constraint constraint)`: Applies a custom validation constraint (alias for `constrain`). -- `AckSchema refine(bool Function(Runtime) validate, {String message})`: Adds custom validation logic with an optional custom error message. -- `CodecSchema transform(R Function(Runtime) transformer)`: Transforms validated runtime values to a different type. +- `AckSchema constrain(Constraint constraint, {String? message})`: Adds a constraint and optionally overrides its message. The constraint must mix in `Validator`, or an `ArgumentError` is thrown. +- `AckSchema withConstraint(Constraint constraint)`: Adds a constraint directly (no message override; `constrain` delegates here). +- `AckSchema refine(bool Function(Runtime) validate, {String message = 'The value did not pass the custom validation.'})`: Adds a custom validation predicate with an optional error message. +- `CodecSchema transform(R Function(Runtime) transformer)`: Transforms validated runtime values to `R` (parse-only; encode fails). ### Utility Methods -- `Map toJsonSchema()`: Renders generic Draft-7 JSON Schema - through the canonical `AckSchemaModel` boundary. +- `Map toJsonSchema()`: Returns a Draft-7 JSON Schema map via the canonical `AckSchemaModel` boundary. - `Map toMap()`: Serializes the schema for debugging. See also [Schema Types](../core-concepts/schemas.mdx) for detailed usage examples. @@ -90,10 +92,10 @@ Schema for validating strings. See [String Validation](../core-concepts/validati ### Pattern Matching -- `matches(String pattern, {String? example, String? message})`: Must match regex pattern. **Note**: Patterns are NOT automatically anchored - use `^...$` for full-string matching. See [String Validation](../core-concepts/validation.mdx#string-constraints) for details. -- `contains(String pattern, {String? example, String? message})`: Must contain pattern anywhere in string (partial matching) -- `startsWith(String value)`: Must start with specified value -- `endsWith(String value)`: Must end with specified value +- `matches(String pattern, {String? example, String? message})`: Must match a regex pattern. Patterns are not automatically anchored — use `^...$` for full-string matching. See [String validation](../core-concepts/validation.mdx#string-constraints) for details. +- `contains(String pattern, {String? example, String? message})`: Must contain the pattern anywhere in the string. +- `startsWith(String value)`: Must start with `value`. +- `endsWith(String value)`: Must end with `value`. ### Format Validation @@ -111,11 +113,6 @@ Schema for validating strings. See [String Validation](../core-concepts/validati - `datetime()`: Must be valid ISO 8601 datetime - `time()`: Must be valid time format (HH:MM:SS) -### Value Restrictions - -- `literal(String value)`: Must exactly match the given value -- `enumString(List values)`: Must be one of the specified values - ### Transformations - `trim()`: Removes leading and trailing whitespace @@ -129,15 +126,17 @@ Schemas for validating numeric values. `IntegerSchema` only accepts `int`, (either `int` or `double`). `DoubleSchema` and `NumberSchema` reject non-finite values by default. See [Number Validation](../core-concepts/validation.mdx#number-constraints). -- `min(num limit)`: Minimum value (inclusive) -- `max(num limit)`: Maximum value (inclusive) -- `greaterThan(num limit)`: Must be greater than limit (exclusive) -- `lessThan(num limit)`: Must be less than limit (exclusive) +Each method's parameter type matches the schema's runtime type: `int` for `IntegerSchema`, `double` for `DoubleSchema`, and `num` for `NumberSchema`. + +- `min(N limit)`: Minimum value (inclusive) +- `max(N limit)`: Maximum value (inclusive) +- `greaterThan(N limit)`: Must be greater than limit (exclusive) +- `lessThan(N limit)`: Must be less than limit (exclusive) - `positive()`: Must be greater than 0 - `negative()`: Must be less than 0 -- `multipleOf(num factor)`: Must be a multiple of the factor -- `finite()`: Must be finite (DoubleSchema and NumberSchema; already the default) -- `safe()`: Must be within safe integer range (IntegerSchema only) +- `multipleOf(N factor)`: Must be a multiple of the factor +- `finite()`: Must be finite (`DoubleSchema` and `NumberSchema`; already the default) +- `safe()`: Must be within safe integer range (`IntegerSchema` only) ## `BooleanSchema` @@ -170,47 +169,51 @@ Schema for validating objects (maps). See [Object Validation](../core-concepts/s Object returned by `safeParse()`. See [Error Handling](../core-concepts/error-handling.mdx). -- `bool isOk`: True if validation succeeded -- `bool isFail`: True if validation failed -- `T? getOrThrow()`: Returns the validated value (nullable for nullable schemas) or throws an exception -- `T? getOrNull()`: Returns the validated value or null if validation failed -- `SchemaError getError()`: Returns the validation error (only valid when `isFail` is true) -- `T? getOrElse(T? Function() orElse)`: Returns the validated value or the fallback result +- `bool isOk`: `true` if validation succeeded. +- `bool isFail`: `true` if validation failed. +- `T? getOrThrow()`: Returns the validated value (which can be `null` for a nullable schema), or throws `AckException` on failure. +- `T? getOrNull()`: Returns the validated value, or `null` on failure. +- `SchemaError getError()`: Returns the validation error; only valid when `isFail` is `true`. +- `T? getOrElse(T? Function() orElse)`: Returns the validated value, or calls `orElse` on failure. +- `SchemaResult map(R Function(T?) transform)`: Maps the successful value to a new result type; propagates failures unchanged. +- `R match({required R Function(T?) onOk, required R Function(SchemaError) onFail})`: Pattern-matches on success or failure. +- `void ifOk(void Function(T?) action)`: Executes `action` only when the result is successful. +- `void ifFail(void Function(SchemaError) action)`: Executes `action` only when the result is a failure. -## `SchemaError` (and Subclasses) +## `SchemaError` (and subclasses) -Object representing a validation failure. See [Error Handling](../core-concepts/error-handling.mdx). +Represents a validation failure. See [Error handling](../core-concepts/error-handling.mdx). -- `String message`: Human-readable error message -- `SchemaContext context`: Context information about where the error occurred +- `String message`: Human-readable error message. +- `SchemaContext context`: Context about where the error occurred. **Subclasses:** -- `SchemaConstraintsError`: Constraint validation failures -- `SchemaNestedError`: Nested validation failures (for objects/arrays) -- `SchemaValidationError`: Custom refinement failures +- `SchemaConstraintsError`: One or more constraint violations. +- `SchemaNestedError`: Validation failures in nested objects or arrays. +- `SchemaValidationError`: Custom refinement failures. +- `SchemaEncodeError`: Encode-path failures (non-nullable null, one-way transform, encoder threw, etc.). ## `Constraint` -Base class for creating custom validation rules. See [Custom Validation](../guides/custom-validation.mdx). +Base class for custom validation rules. See [Custom validation](../guides/custom-validation.mdx). -- `String constraintKey`: Unique key for the constraint -- `String description`: Human-readable description -- `Map toMap()`: Serializes the constraint for debugging +- `String constraintKey`: Unique identifier for the constraint. +- `String description`: Human-readable description. +- `Map toMap()`: Serializes the constraint for debugging. -## `Validator` (Mixin) +## `Validator` (mixin) Validation behavior mixin used with `Constraint`. -- `bool isValid(T value)`: Returns `true` when the value passes validation -- `String buildMessage(T value)`: Builds the validation failure message -- `ConstraintError? validate(T value)`: Validates a value and returns an error if invalid +- `bool isValid(T value)`: Returns `true` when the value passes validation. +- `String buildMessage(T value)`: Builds the validation failure message. +- `ConstraintError? validate(T value)`: Validates a value and returns an error if invalid. -## Additional Schema Types +## Additional schema types Ack ships with a broad set of schema factories beyond what is listed here. -See [Schema Types](../core-concepts/schemas.mdx) for the full catalogue, -including helpers like `Ack.date()`, `Ack.literal()`, and rich list/object -combinators. +See [Schema types](../core-concepts/schemas.mdx) for the full catalogue, +including `Ack.date()`, `Ack.literal()`, and list/object combinators. ### `Ack.discriminated(...)` @@ -238,14 +241,12 @@ Schema reference for recursive object graphs. - Bare or wrapped lazy schemas cannot be used as discriminated-union branches because the branch discriminator must be analyzable at construction time. -## Code Generation Annotations +## Code generation annotations -Use the [`ack_generator`](https://pub.dev/packages/ack_generator) builder to -turn annotations into extension types. After adding -the annotations below, run: +Use the [`ack_generator`](https://pub.dev/packages/ack_generator) builder to turn annotations into extension types. After adding the annotations below, run: ```bash -dart run build_runner build +dart run build_runner build --delete-conflicting-outputs ``` ### `@AckType()` @@ -254,7 +255,7 @@ dart run build_runner build **Generates**: An extension type wrapper around the existing schema -Annotate an existing schema variable or getter to generate an extension type wrapper. The schema itself stays in your source file. +Annotate a schema variable or getter to generate an extension type wrapper. The schema stays in your source file. **Supported schema types:** - `Ack.object({...})` → Object extension types @@ -288,15 +289,11 @@ print(user.email); // Type-safe String access ### `EnumSchema` -Schema for validating enum values. - -- Created using `Ack.enumValues(List values)` where T extends Enum +Schema for validating enum values. Created using `Ack.enumValues(List values)` where `T extends Enum`. ### `AnyOfSchema` -Schema for union types (value must match one of several schemas). - -- Created using `Ack.anyOf(List schemas)` +Schema for union types; the value must match one of several schemas. Created using `Ack.anyOf(List schemas)`. ### `DiscriminatedObjectSchema` @@ -312,37 +309,45 @@ Schema for polymorphic validation based on a discriminator field. Canonical export model for Ack schemas. -- Created using `schema.toSchemaModel()` -- Represents the boundary/export shape, JSON-compatible defaults, - discriminator metadata, target-independent constraints, and export warnings -- Render generic Draft-7 JSON Schema with - `schema.toSchemaModel().toJsonSchema()`, the same output returned by - `schema.toJsonSchema()` -- Adapter packages that need a JSON map can call `schema.toJsonSchema()`; - adapters for non-JSON targets should convert from `AckSchemaModel` rather - than traversing `AckSchema` subclasses directly +- Created by `schema.toSchemaModel()`. +- Represents the boundary/export shape, JSON-compatible defaults, discriminator metadata, target-independent constraints, and export warnings. +- `schema.toSchemaModel().toJsonSchema()` returns the same Draft-7 map as `schema.toJsonSchema()`. +- Adapters that need a JSON map should call `schema.toJsonSchema()`; adapters for non-JSON targets should convert from `AckSchemaModel` rather than traversing `AckSchema` subclasses directly. ### `AnySchema` -Schema that accepts any non-null JSON-safe value without validation. +Accepts any non-null JSON-safe value without further validation. -- Created using `Ack.any()` -- Useful for dynamic payloads or pass-through metadata +- Created by `Ack.any()`. +- Useful for dynamic payloads or pass-through metadata. ### `CodecSchema` Schema that decodes boundary values into runtime values and encodes runtime -values back to the boundary representation. +values back to the boundary representation. Use `encode` / `safeEncode` for the +reverse direction. See [Codecs](../core-concepts/codecs.mdx). + +- `schema.transform(R Function(Runtime) transformer)`: one-way transform + (parse only; `encode` fails with a one-way error). +- `schema.codec({required R Function(Runtime) decode, required Runtime Function(R) encode, AckSchema? output})`: + bidirectional codec; the optional `output` schema validates the runtime value. +- `Ack.codec({required input, required decode, required encode, output})`: + builds a codec from an `input` schema. + +**Built-in codecs:** -- Created using `schema.transform(R Function(Runtime) transformer)` or - `schema.codec(decode: ..., encode: ...)` +- `Ack.date()` → `CodecSchema` (ISO `YYYY-MM-DD` ↔ local-midnight `DateTime`) +- `Ack.datetime()` → `CodecSchema` (ISO 8601 ↔ UTC `DateTime`) +- `Ack.uri()` → `CodecSchema` (absolute URI string ↔ `Uri`) +- `Ack.duration()` → `CodecSchema` (milliseconds ↔ `Duration`) +- `Ack.enumCodec(List values)` → `CodecSchema` (enum `.name` ↔ enum value) -### Optional Schemas +### Optional schemas -Every schema can be marked optional through the `optional({bool value = true})` fluent API. +Every schema can be marked optional via the `optional({bool value = true})` fluent API. -- Calling `schema.optional()` sets `isOptional` to `true` without wrapping the schema. -- Use `schema.optional(value: false)` to clear the optional flag. +- `schema.optional()` sets `isOptional` to `true` without wrapping the schema. +- `schema.optional(value: false)` clears the optional flag. - Optional affects object-field presence only; combine with `.nullable()` to also allow explicit `null`. -*Refer to the [Schema Types](../core-concepts/schemas.mdx) guide for detailed usage and examples.* +*See the [Schema types](../core-concepts/schemas.mdx) guide for detailed usage and examples.* diff --git a/docs/core-concepts/codecs.mdx b/docs/core-concepts/codecs.mdx new file mode 100644 index 00000000..825811aa --- /dev/null +++ b/docs/core-concepts/codecs.mdx @@ -0,0 +1,111 @@ +--- +title: Codecs +--- + +Your API sends a timestamp as an ISO string, but your code wants a `DateTime`. A codec handles that round-trip: it validates the incoming string, decodes it into the Dart type you actually use, and encodes it back when you send data out. + +```dart +final when = Ack.datetime(); // CodecSchema + +final dt = when.parse('2026-01-01T00:00:00Z'); // decode: String -> DateTime +final iso = when.encode(dt); // encode: DateTime -> String +``` + +Ack calls the wire shape the **boundary** (JSON-safe primitives like `String` or `int`) and the Dart value the **runtime** (a `DateTime`, `Uri`, `Duration`, or your own type). Parsing decodes boundary → runtime; encoding goes the other way. + +## Transform vs. codec + +| Helper | Direction | Returns | +| --- | --- | --- | +| `schema.transform(fn)` | one-way (parse only) | `CodecSchema` | +| `schema.codec(decode: ..., encode: ...)` | bidirectional | `CodecSchema` | + +Use `transform` when you only ever decode. Encoding a transformed schema fails +with a `SchemaEncodeError` (a one-way transform has no encoder). Use `codec` +when you need a reversible encode path. + +## Built-in codecs + +Ack ships codecs for the most common boundary conversions: + +| Factory | Boundary ↔ Runtime | Runtime invariant | +| --- | --- | --- | +| `Ack.date()` | ISO `YYYY-MM-DD` `String` ↔ `DateTime` | local midnight (no time-of-day) | +| `Ack.datetime()` | ISO 8601 `String` ↔ `DateTime` | must be UTC | +| `Ack.uri()` | `String` ↔ `Uri` | absolute URI (scheme + host) | +| `Ack.duration()` | milliseconds `int` ↔ `Duration` | whole milliseconds | +| `Ack.enumCodec(values)` | enum-name `String` ↔ enum value | one of `values` | + +```dart +final schema = Ack.object({ + 'startsAt': Ack.datetime(), + 'website': Ack.uri(), + 'timeout': Ack.duration(), +}); + +final event = schema.parse({ + 'startsAt': '2026-01-01T09:00:00Z', + 'website': 'https://example.com', + 'timeout': 5000, +}); + +// event['startsAt'] is a DateTime (UTC) +// event['website'] is a Uri +// event['timeout'] is a Duration (5 seconds) +``` + +Each built-in enforces a runtime invariant on encode. `Ack.datetime()` requires a UTC `DateTime`; convert local values with `.toUtc()` before encoding. + +## Custom codecs + +Create a codec from any input schema with `Ack.codec(...)`, providing a `decode` function (boundary → runtime) and an `encode` function (runtime → boundary): + +```dart +final csv = Ack.codec>( + input: Ack.string(), + decode: (s) => s.split(','), + encode: (list) => list.join(','), +); + +csv.parse('a,b,c'); // ['a', 'b', 'c'] +csv.encode(['a', 'b', 'c']); // 'a,b,c' +``` + +The three type arguments are the boundary type, the input schema's runtime type, and the final runtime type. The optional `output` schema validates the decoded value (defaults to a type check on the runtime type). + +Call `.codec(...)` on an existing schema to add a reversible conversion: + +```dart +final trimmed = Ack.string().codec( + decode: (s) => s.trim(), + encode: (s) => s, +); +``` + +## Encoding back to the boundary + +Use `encode` (throws on failure) or `safeEncode` (returns a `SchemaResult`): + +```dart +final result = Ack.datetime().safeEncode(DateTime.utc(2026)); +if (result.isOk) { + print(result.getOrThrow()); // 2026-01-01T00:00:00.000Z +} +``` + +Errors surface as typed [`SchemaError`](./error-handling.mdx) values: + +- `SchemaTransformError` — a `decode` function threw during parsing. +- `SchemaEncodeError` — encoding failed (for example, a one-way `transform`, or a + runtime invariant violation such as a non-UTC `DateTime` for `Ack.datetime()`). + +## Codecs and JSON Schema + +A codec exports the JSON Schema of its **boundary** (input) schema — that is the shape that crosses the wire. For example, `Ack.datetime()` exports as `string` with `format: date-time`. The runtime type and decode/encode logic are not part of the exported schema. See [JSON Serialization](./json-serialization.mdx). + +## Next steps + +- **[Schema Types](./schemas.mdx)**: Schema types and the `transform` helper +- **[Validation Rules](./validation.mdx)**: Built-in constraints +- **[Error Handling](./error-handling.mdx)**: `SchemaError` and `SchemaResult` +- **[JSON Serialization](./json-serialization.mdx)**: Exporting schemas diff --git a/docs/core-concepts/configuration.mdx b/docs/core-concepts/configuration.mdx index 760ade3f..03509647 100644 --- a/docs/core-concepts/configuration.mdx +++ b/docs/core-concepts/configuration.mdx @@ -2,19 +2,13 @@ title: Configuration --- -Ack allows for some configuration settings, although most behavior is controlled directly through the schema definitions when they are created. +Ack has no global configuration object. All behavior is configured at the schema level through the fluent API. -## Global Settings (Less Common) +## Schema-level configuration -In general, Ack prefers configuration per schema definition rather than global settings. However, if needed, you might manage global aspects like custom format registries or shared validation logic through your own application structure (e.g., dependency injection or service patterns). +Define behavior when you create your schemas. -*Currently, Ack does not expose a dedicated global configuration object. Configuration is primarily done at the schema level.* - -## Schema-Level Configuration - -Most "configuration" happens when you define your schemas using the fluent API provided by Ack. - -### Required Fields +### Required fields Fields in [`Ack.object`](./schemas.mdx#object) are required by default. Mark a field as optional with `.optional()`. @@ -26,42 +20,33 @@ Ack.object({ }); // id and name are required by default ``` -### Additional Properties +### Additional properties -Control how extra fields (not defined in the schema) are handled using the `additionalProperties` parameter in [`Ack.object`](./schemas.mdx#object). +Control how extra fields are handled with the `additionalProperties` parameter in [`Ack.object`](./schemas.mdx#object). ```dart -// Disallow any extra properties (default behavior) -Ack.object({ - 'id': Ack.integer() -}, additionalProperties: false); -// or simply: -Ack.object({ - 'id': Ack.integer() -}); +// Reject extra properties (default) +Ack.object({'id': Ack.integer()}); +// or explicitly: +Ack.object({'id': Ack.integer()}, additionalProperties: false); -// Allow any extra properties -Ack.object({ - 'id': Ack.integer() -}, additionalProperties: true); +// Allow extra properties +Ack.object({'id': Ack.integer()}, additionalProperties: true); ``` -### Custom Error Messages +### Custom error messages -Built-in validation rules provide default error messages. For custom error messages, use custom constraints with the `.constrain()` method. +Built-in constraints provide default messages. To override them, use `.constrain()`. See also: [Custom Error Messages in Error Handling](./error-handling.mdx#custom-error-messages). ```dart -// Built-in constraints use default messages Ack.string().minLength(5); // Default: "Too short, min 5 characters" -Ack.integer().min(18); // Default: "Must be at least 18" - -// For custom messages, use custom constraints (see Custom Validation section below) +Ack.integer().min(18); // Default: "Must be at least 18" ``` -### Custom Validation Logic +### Custom validation logic -Use `.constrain()` to add reusable validation logic for value-level checks, or `.refine()` for cross-field rules. +Use `.constrain()` for reusable value-level checks, or `.refine()` for cross-field rules. ```dart import 'package:ack/ack.dart'; @@ -91,13 +76,12 @@ final signUpSchema = Ack.object({ message: 'Passwords do not match', ); ``` -*See the [Custom Validation Guide](../guides/custom-validation.mdx) for additional patterns.* +*See the [Custom Validation Guide](../guides/custom-validation.mdx) for more patterns.* -## AckType Generation +## AckType generation `ack_generator` works with top-level schemas annotated with `@AckType()`. Write -the schema directly, then generate an extension type wrapper around its -validated representation: +the schema, then generate an extension type wrapper around its validated representation: ```dart import 'package:ack/ack.dart'; @@ -113,15 +97,6 @@ final userSchema = Ack.object({ }, additionalProperties: true); ``` -After running `dart run build_runner build`, the generated part adds a typed -wrapper such as `UserType` with `parse()` / `safeParse()` helpers and typed -getters for the schema fields. - -*See [TypeSafe Schemas](./typesafe-schemas.mdx) and the generator README for -setup details and supported schema shapes.* - -## Summary +After running `dart run build_runner build`, the generated part adds `UserType` with `parse()` / `safeParse()` helpers and typed getters. -- Ack primarily uses **schema-level configuration** through methods and parameters like `.minLength()`, `.optional()`, `additionalProperties: ...`. -- **Manual schema definition** is the recommended approach for production applications. -- There is currently no central **global configuration** object provided by Ack itself. +*See [TypeSafe Schemas](./typesafe-schemas.mdx) and the generator README for setup details and supported schema shapes.* diff --git a/docs/core-concepts/error-handling.mdx b/docs/core-concepts/error-handling.mdx index 89e0e40f..39604d71 100644 --- a/docs/core-concepts/error-handling.mdx +++ b/docs/core-concepts/error-handling.mdx @@ -2,18 +2,21 @@ title: Error Handling --- -Ack provides detailed error information when validation fails. This guide explains how to interpret and use these errors. +When validation fails, you want to know what went wrong and where. `safeParse()` hands back a `SchemaResult` holding either the validated value or a `SchemaError`, so you can inspect failures without try/catch. -## The `SchemaResult` Object +## The `SchemaResult` object -All `safeParse()` methods in Ack return a `SchemaResult` object. This object encapsulates either the successfully validated data or a `SchemaError`. +`safeParse()` returns a `SchemaResult`. Its value is nullable — a nullable schema can succeed with `null`. -- `result.isOk`: Returns `true` if validation succeeded, `false` otherwise. -- `result.isFail`: Returns `true` if validation failed, `false` otherwise. -- `result.getOrThrow()`: Returns the validated data if `isOk` is true, otherwise throws an exception. -- `result.getOrNull()`: Returns the validated data if `isOk` is true, otherwise returns `null`. -- `result.getError()`: Returns the `SchemaError` if `isFail` is true, otherwise throws an exception. -- `result.getOrElse(() => defaultValue)`: Returns the validated data if `isOk` is true, otherwise calls the fallback function. +- `result.isOk`: `true` if validation succeeded. +- `result.isFail`: `true` if validation failed. +- `result.getOrThrow()`: Returns the validated value, or throws `AckException` on failure. +- `result.getOrNull()`: Returns the validated value, or `null` on failure. +- `result.getError()`: Returns the `SchemaError` on failure, or throws `StateError` if called on a success. +- `result.getOrElse(() => defaultValue)`: Returns the validated value on success, or calls the fallback on failure. +- `result.match(onOk: (value) => ..., onFail: (error) => ...)`: Collapses both cases into a single value. +- `result.map((value) => ...)`: Transforms the success value and leaves failures untouched. +- `result.ifOk((value) { ... })` / `result.ifFail((error) { ... })`: Runs a side effect for just one case. ```dart import 'package:ack/ack.dart'; @@ -40,12 +43,12 @@ if (result.isFail) { ## Understanding `SchemaError` -The `SchemaError` object contains details about the validation failure. +`SchemaError` contains details about the validation failure. -- `error.name`: The schema/context name where validation failed (for example, a field name or your `debugName`). -- `error.value`: The actual value that failed validation. -- `error.schema`: The schema that was being validated against. -- `error.path`: The JSON Pointer path to the failing location (for example, `#/address/city`). +- `error.name`: The context name at the failure point (for example, a field name or the `debugName` passed to `safeParse`). +- `error.value`: The value that failed validation. +- `error.schema`: The schema being validated against. +- `error.path`: The RFC 6901 JSON Pointer path to the failure (for example, `#/address/city`). ```dart final userSchema = Ack.object({ @@ -79,13 +82,13 @@ if (result.isFail) { } ``` -## Error Types +## Error types -Ack uses specific error types that inherit from `SchemaError`. Each error type provides detailed information about why validation failed. +Each `SchemaError` subtype carries specific information about why validation failed. ### `TypeMismatchError` -Occurs when the input value type doesn't match the expected schema type. +Raised when the input type doesn't match the expected schema type. ```dart final schema = Ack.string(); @@ -101,7 +104,7 @@ if (result.isFail) { ### `SchemaConstraintsError` -Occurs when the value violates one or more validation constraints (e.g., `minLength`, `min`, `email`). +Raised when the value violates one or more validation constraints (such as `minLength`, `min`, or `email`). ```dart final schema = Ack.string().minLength(5).email(); @@ -121,7 +124,7 @@ if (result.isFail) { ### `SchemaNestedError` -Occurs when validation fails within nested objects or lists. Contains a list of child errors. +Raised when validation fails within nested objects or lists. Contains a list of child errors. ```dart final schema = Ack.object({ @@ -148,7 +151,7 @@ if (result.isFail) { ### `SchemaValidationError` -Occurs when custom validation logic added via `.refine()` fails. +Raised when custom validation logic added via `.refine()` fails. ```dart final schema = Ack.integer().refine( @@ -166,9 +169,9 @@ if (result.isFail) { ### `SchemaTransformError` -Occurs when a transformation function (from `.transform()`) throws an exception. +Raised when a `.transform()` callback throws an exception. -Transform callbacks receive the non-null validated runtime value; null handling is owned by the surrounding schema's `nullable`/`withDefault` configuration. A `SchemaTransformError` is raised when the callback itself throws — for example, when the input shape passes validation but the conversion fails: +Callbacks receive the non-null validated runtime value; null handling belongs to the surrounding `nullable`/`withDefault` configuration. `SchemaTransformError` fires when the callback itself throws — for example, when the input passes validation but the conversion fails: ```dart final schema = Ack.string().transform((value) { @@ -186,9 +189,13 @@ if (result.isFail) { } ``` -### Working with Error Types +### `SchemaEncodeError` -Use pattern matching or type checks to handle different error types: +Raised when `encode()` or `safeEncode()` can't turn a runtime value back into its boundary form — for example, encoding a one-way `transform`, or a value that breaks a codec's invariant (such as a non-UTC `DateTime` for `Ack.datetime()`). Its `kind` field says which case occurred. + +### Working with error types + +Use pattern matching to handle specific error types: ```dart final result = schema.safeParse(data); @@ -207,51 +214,51 @@ if (result.isFail) { print('Custom validation failed: ${error.message}'); case SchemaTransformError(): print('Transformation failed: ${error.cause}'); + case SchemaEncodeError(): + print('Could not encode value: ${error.message}'); default: print('Validation failed: ${error.message}'); } } ``` -## Displaying Errors in UI (Flutter Example) +## Displaying errors in UI (Flutter example) -When using Ack with Flutter forms, you can extract the error message for display. +In a `TextFormField` validator, return the error string directly: ```dart // Inside TextFormField validator validator: (value) { final result = someSchema.safeParse(value); if (result.isFail) { - // Return the error details for the UI return result.getError().toString(); } - return null; // Return null if valid + return null; } ``` -*See the [Form Validation Guide](../guides/flutter-form-validation.mdx) for more details.* +*See the [Form Validation Guide](../guides/flutter-form-validation.mdx) for details.* -## Custom Error Messages +## Custom error messages -Built-in constraints provide default error messages. For custom error messages, create custom constraints using the `.constrain()` method. See [Custom Validation Guide](../guides/custom-validation.mdx) for details. +Built-in constraints ship with default messages. For a custom message, pass `message:` to `.refine()`, or attach a constraint with `.constrain(..., message: ...)` — see [Custom Validation](../guides/custom-validation.mdx). ```dart -// Built-in constraints use default messages -final schema = Ack.string().minLength(5); +final schema = Ack.string().refine( + (value) => value.startsWith('ACK_'), + message: 'Value must start with ACK_', +); -final result = schema.safeParse('abc'); +final result = schema.safeParse('nope'); if (result.isFail) { - final error = result.getError(); - print(error.toString()); // Output: validation error details + print(result.getError().message); // Value must start with ACK_ } ``` -## Next Steps - -Learn more about related topics: +## Next steps -- **[Custom Validation](/guides/custom-validation)**: Create custom constraints with tailored error messages +- **[Custom Validation](/guides/custom-validation)**: Create constraints with custom error messages - **[Flutter Forms](/guides/flutter-form-validation)**: Display validation errors in Flutter form widgets -- **[Validation Rules](/core-concepts/validation)**: Explore all built-in validation constraints -- **[Schema Types](/core-concepts/schemas)**: Learn about different schema types and their behavior -- **[Common Recipes](/guides/common-recipes)**: See practical error handling patterns +- **[Validation Rules](/core-concepts/validation)**: All built-in validation constraints +- **[Schema Types](/core-concepts/schemas)**: Schema types and their behavior +- **[Common Recipes](/guides/common-recipes)**: Practical error handling patterns diff --git a/docs/core-concepts/json-serialization.mdx b/docs/core-concepts/json-serialization.mdx index fff88a9d..f10e4c7b 100644 --- a/docs/core-concepts/json-serialization.mdx +++ b/docs/core-concepts/json-serialization.mdx @@ -2,15 +2,14 @@ title: JSON Serialization --- -Ack schemas validate JSON data and let you work with validated data directly, -wrap it with generated types, or hand it off to your own model layer. +Ack schemas validate JSON data and let you work with the validated data directly, wrap it with generated types, or pass it to your own model layer. -## Validating JSON Data +## Validating JSON data -The most common use case is validating incoming JSON data (e.g., from an API response). This typically involves two steps: +Validating incoming JSON data involves two steps: 1. **Decode JSON:** Use `dart:convert` to parse the JSON string into a Dart object (usually `Map` or `List`). -2. **Validate:** Pass the decoded Dart object to your [Ack schema](./schemas.mdx)'s `safeParse()` method. +2. **Validate:** Pass the decoded object to your [Ack schema](./schemas.mdx)'s `safeParse()` method. ```dart import 'dart:convert'; @@ -38,18 +37,15 @@ void processApiResponse(String jsonString) { final result = userSchema.safeParse(jsonData); if (result.isOk) { - // Data structure and types are valid according to the schema. + // Structure and types are valid. final validDataMap = result.getOrThrow(); print('Valid JSON received: $validDataMap'); - - // Optionally hand the validated map to your own model layer - // or use an AckType-generated wrapper (see next section). + // Pass the validated map to your own model layer, + // or use an AckType-generated wrapper (see next section). } else { - // Handle validation errors (see Error Handling guide) + // Handle validation errors (see the Error Handling guide). print('Invalid JSON data: ${result.getError()}'); - // Log the full error for debugging if needed: - // print('Error details: ${result.getError()}'); } } @@ -62,12 +58,9 @@ processApiResponse('not valid json'); // Decoding error *Learn more about [Error Handling](./error-handling.mdx).* -## Working with Validated Data +## Working with validated data -After successful validation, `result.getOrThrow()` returns a -`Map` whose structure and types match your schema. You can -work with this validated data directly, pass it into your own model class, or -use a generated typed wrapper: +After successful validation, `result.getOrThrow()` returns a `Map` whose structure and types match your schema. You can work with it directly, pass it into a model class, or use a generated typed wrapper: ```dart final result = userSchema.safeParse(jsonData); @@ -88,58 +81,25 @@ if (result.isOk) { } ``` -## Generating Extension Types with `@AckType()` +## Parsing JSON into typed wrappers -When you write schemas manually, annotate the variable or getter with -`@AckType()` to generate a typed wrapper around the validated data. - -```dart -// user_schema.dart -import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; - -part 'user_schema.g.dart'; - -@AckType() -final userSchema = Ack.object({ - 'name': Ack.string(), - 'email': Ack.string().email().nullable(), -}); -``` - -Running `dart run build_runner build` generates in `user_schema.g.dart`: - -- **The original schema constant** stays in your source file (`user_schema.dart`) -- **Extension type** `UserType` with `parse`/`safeParse` and typed getters - -The type name is derived from the variable name by removing "Schema" suffix and adding "Type" (e.g., `userSchema` → `UserType`). +Once a schema is annotated with `@AckType()` (see [TypeSafe Schemas](./typesafe-schemas.mdx) for setup), parse JSON straight into typed getters — no manual casting: ```dart import 'dart:convert'; -final jsonString = '{"name": "Alice", "email": "alice@example.com"}'; -final jsonData = jsonDecode(jsonString); - +final jsonData = jsonDecode('{"name": "Alice", "email": "alice@example.com"}'); final result = UserType.safeParse(jsonData); if (result.isOk) { - final user = result.getOrThrow(); - print(user.name); // Type-safe String access - print(user.email); // Type-safe String? access + final user = result.getOrThrow()!; + print(user.name); // typed String + print(user.email); // typed String? } ``` -**Note**: Nested schemas annotated with `@AckType()` generate their own -extension types. Object/list references must be statically resolvable for typed -generation; unsupported or unresolved references fail generation instead of -falling back to `Map`. For current `Ack.discriminated(...)` -limits, see -[Type-safe Schemas](./typesafe-schemas.mdx#discriminated-schemas). - -## Key Considerations +## Key considerations -- **`dart:convert`:** Use the standard `dart:convert` library (`jsonEncode`, `jsonDecode`) for JSON string conversion. Ack handles validation only. -- **Type Safety:** While `jsonDecode` produces `dynamic`, successful validation gives you confidence in the structure and types of the resulting `Map`. -- **App-Level Model Conversion:** After validation, how you turn the validated - Map into an app-specific model is up to you. Ack keeps validation and - wrapper generation separate from your own model layer. +- **`dart:convert`:** Use `jsonEncode`/`jsonDecode` for JSON string conversion. Ack handles validation only. +- **Type safety:** `jsonDecode` produces `dynamic`, but successful validation guarantees the structure and types of the resulting `Map`. +- **Model conversion:** After validation, how you convert the validated map into an app model is up to you. Ack keeps validation and wrapper generation separate from your model layer. diff --git a/docs/core-concepts/schemas.mdx b/docs/core-concepts/schemas.mdx index 88871a8d..b09f2ffb 100644 --- a/docs/core-concepts/schemas.mdx +++ b/docs/core-concepts/schemas.mdx @@ -2,11 +2,7 @@ title: Schemas --- -This guide covers schema types, validation, and working with validated data in Ack. - -## Overview - -Ack provides a schema validation system built around the `AckSchema` base class. Use the `Ack` factory to create schemas, then validate data with the `safeParse()` method. +A schema describes the shape your data should have. You build one with the `Ack` factory, then validate input against it. This page tours every schema type and how to compose them. ```dart import 'package:ack/ack.dart'; @@ -33,7 +29,7 @@ if (result.isOk) { } ``` -## Schema Types +## Schema types ### String @@ -62,6 +58,11 @@ enum Role { admin, user, guest } final roleSchema = Ack.enumValues(Role.values); ``` +> **`Ack.string().date()` vs `Ack.date()`:** `Ack.string().date()` checks the +> format and keeps the value a `String`. [`Ack.date()`](./codecs.mdx) (a codec) +> validates the same format but returns a `DateTime`. The same applies to +> `Ack.string().datetime()` vs `Ack.datetime()`. + ### Number Numeric schemas are strict about their Dart runtime type. `Ack.integer()` @@ -83,7 +84,6 @@ final priceSchema = Ack.double() // Either int or double final amountSchema = Ack.number().positive(); -// Negative/positive final temperatureSchema = Ack.integer(); // Any integer final scoreSchema = Ack.double().positive(); // > 0 final debtSchema = Ack.double().negative(); // < 0 @@ -126,7 +126,7 @@ final userSchema = Ack.object({ }); ``` -**Nested Objects:** +**Nested objects:** ```dart final userSchema = Ack.object({ @@ -139,7 +139,7 @@ final userSchema = Ack.object({ }); ``` -**Working with Validated Data:** +**Working with validated data:** ```dart final result = userSchema.safeParse(data); @@ -154,7 +154,7 @@ if (result.isOk) { } ``` -### Union Types +### Union types Validate against multiple possible schemas: @@ -184,7 +184,7 @@ final shapeSchema = Ack.discriminated( The union owns the discriminator and injects the exact branch literal at parse/export boundaries. Branch schemas usually omit the discriminator field. -### Recursive Schemas +### Recursive schemas Use `Ack.lazy(...)` when a schema needs to refer to itself: @@ -201,11 +201,11 @@ categorySchema = Ack.object({ The lazy builder is resolved once and memoized. JSON Schema export renders the reference through Draft-7 `definitions` / `$ref`, so recursive children are -referenced instead of inlined forever. +referenced rather than inlined forever. ### Any -Accept any non-null JSON-safe value without validation (use sparingly): +Accepts any non-null JSON-safe value without validation (use sparingly): ```dart final flexibleSchema = Ack.object({ @@ -214,13 +214,11 @@ final flexibleSchema = Ack.object({ }); ``` -Use `Ack.any().nullable()` to accept `null`. - -## Optional vs Nullable +Use `Ack.any().nullable()` to also accept `null`. -Understanding the difference is crucial: +## Optional vs nullable -**`.nullable()`** - Field must be present but can be `null`: +**`.nullable()`** — Field must be present but can be `null`: ```dart final userSchema = Ack.object({ @@ -236,7 +234,7 @@ final userSchema = Ack.object({ {'name': 'John'} ``` -**`.optional()`** - Field can be completely omitted (but is still validated when present): +**`.optional()`** — Field can be omitted (but is still validated when present): ```dart final userSchema = Ack.object({ @@ -252,7 +250,7 @@ final userSchema = Ack.object({ {'name': 'John', 'age': null} // Use .nullable() if null should be allowed ``` -**Combining both** - Field can be missing OR null: +**Combining both** — Field can be missing or `null`: ```dart final userSchema = Ack.object({ @@ -266,7 +264,7 @@ final userSchema = Ack.object({ {'name': 'John', 'bio': 'Developer'} ``` -## Object Schema Operations +## Object schema operations ### Extension @@ -290,7 +288,7 @@ final modifiedSchema = baseSchema.extend({ }); ``` -### Pick and Omit +### Pick and omit Select or exclude properties: @@ -330,11 +328,11 @@ partialSchema.safeParse({'name': 'John'}); partialSchema.safeParse({'email': 'john@example.com', 'age': 30}); ``` -### Additional Properties +### Additional properties -Control handling of extra properties not defined in the schema. By default, objects are **strict** and reject additional properties. +By default, objects are **strict** and reject additional properties. -#### Using Constructor Parameter +#### Using the constructor parameter ```dart // Strict mode (default) - rejects additional properties @@ -354,7 +352,7 @@ final flexibleSchema = Ack.object({ flexibleSchema.safeParse({'id': '1', 'name': 'John', 'extra': 'allowed'}); // ✅ Passes ``` -#### Using Extension Methods +#### Using extension methods ```dart final baseSchema = Ack.object({ @@ -371,12 +369,12 @@ final passthrough = baseSchema.passthrough(); passthrough.safeParse({'id': '1', 'name': 'John', 'role': 'admin'}); // ✅ Passes ``` -**Common Use Cases:** +**Common use cases:** -- **Strict mode**: API request validation, form validation where only known fields are allowed -- **Passthrough mode**: Working with dynamic data or when extra metadata is acceptable +- **Strict mode**: API request validation and form validation where only known fields are allowed +- **Passthrough mode**: Dynamic data or cases where extra metadata is acceptable -## Custom Validation +## Custom validation ### Refinements @@ -417,7 +415,7 @@ final orderSchema = Ack.object({ ### Transformations -Transform validated data: +Transform validated data after parsing: ```dart // Transform to uppercase. The callback receives the non-null validated @@ -437,39 +435,17 @@ final userWithAgeSchema = Ack.object({ // Type transformation final dateSchema = Ack.string() .matches(r'\d{4}-\d{2}-\d{2}') - .transform((s) => DateTime.parse(s!)); -``` - -## Validation Result - -The `safeParse()` method returns a `SchemaResult`: - -```dart -final result = schema.safeParse(data); - -// Check success -if (result.isOk) { - final data = result.getOrThrow(); -} - -// Check failure -if (result.isFail) { - final error = result.getError(); -} - -// Other methods -final data = result.getOrNull(); // null if failed -final data = result.getOrElse(() => defaultValue); // default if failed + .transform((s) => DateTime.parse(s)); ``` -*See the [Error Handling](./error-handling.mdx) guide for details on working with errors.* +## Validation result -## Next Steps +Every `safeParse()` returns a `SchemaResult` holding the validated value or a `SchemaError`. See [Error Handling](./error-handling.mdx) for reading values and errors. -Explore related topics to deepen your understanding: +## Next steps -- **[Validation Rules](./validation.mdx)**: Master all built-in constraints and strict parsing -- **[Error Handling](./error-handling.mdx)**: Handle validation errors effectively -- **[Custom Validation](../guides/custom-validation.mdx)**: Create custom constraints and refinements -- **[JSON Serialization](./json-serialization.mdx)**: Validate and transform JSON data -- **[Common Recipes](../guides/common-recipes)**: See practical schema patterns +- **[Validation rules](./validation.mdx)**: All built-in constraints and strict parsing +- **[Error handling](./error-handling.mdx)**: Handle validation errors effectively +- **[Custom validation](../guides/custom-validation.mdx)**: Create custom constraints and refinements +- **[JSON serialization](./json-serialization.mdx)**: Validate and transform JSON data +- **[Common recipes](../guides/common-recipes)**: Practical schema patterns diff --git a/docs/core-concepts/typesafe-schemas.mdx b/docs/core-concepts/typesafe-schemas.mdx index 268b87c6..9e8589b3 100644 --- a/docs/core-concepts/typesafe-schemas.mdx +++ b/docs/core-concepts/typesafe-schemas.mdx @@ -2,16 +2,14 @@ title: TypeSafe Schemas --- -Ack generates extension types from top-level schemas annotated with -`@AckType()`. The schema stays in your source file; the generator adds a typed -wrapper around its validated representation. +Tired of writing `data['name'] as String` after every parse? Annotate a top-level schema with `@AckType()` and run the generator once to get typed getters like `user.name`. The schema stays in your source file; the generator adds a typed wrapper around its validated representation. ## Overview -- Define schemas directly with the Ack fluent API. -- Annotate the top-level schema variable or getter with `@AckType()`. -- Run `dart run build_runner build`. -- Use the generated `TypeName.parse()` / `TypeName.safeParse()` helpers. +1. Define schemas with the Ack fluent API. +2. Annotate each top-level schema variable or getter with `@AckType()`. +3. Run `dart run build_runner build --delete-conflicting-outputs`. +4. Use the generated `TypeName.parse()` / `TypeName.safeParse()` helpers. ## Basic usage @@ -35,39 +33,31 @@ final userSchema = Ack.object({ }); ``` -The generated part file contains: +The generated part file contains `AddressType` and `UserType` extension types, each with typed field getters and `parse()` / `safeParse()` static methods. -- `AddressType(Map _data)` -- `UserType(Map _data)` -- typed getters for each field -- `parse()` and `safeParse()` factories +The type name drops a trailing `Schema` and adds `Type` (`userSchema` → `UserType`). Override it with `@AckType(name: 'AppUser')`, which generates `AppUserType`. ## Supported schema shapes -`@AckType()` works with: +`@AckType()` supports: - `Ack.object(...)` -- primitive schemas such as `Ack.string()`, `Ack.integer()`, `Ack.double()`, `Ack.boolean()` +- `Ack.string()`, `Ack.integer()`, `Ack.double()`, `Ack.boolean()` - `Ack.list(...)` - `Ack.literal(...)`, `Ack.enumString(...)`, `Ack.enumValues(...)` - non-object transforms with explicit output types - `Ack.discriminated(...)` with the constraints below -Unsupported helpers include `Ack.any()` and `Ack.anyOf()`. +`Ack.any()` and `Ack.anyOf()` are not supported. ## Discriminated schemas -`Ack.discriminated(...)` works with `@AckType()` when: +`Ack.discriminated(...)` works with `@AckType()` when all of the following hold: - `schemas` is a non-empty map literal -- the base schema is not nullable -- each branch is a top-level schema variable or getter -- each branch is an `@AckType` object schema -- each branch is non-nullable -- each branch is declared in the same library as the base -- branch schemas normally omit the discriminator field -- if a branch includes the discriminator field, it must be `Ack.literal(...)` - matching the branch key or `Ack.enumString(...)` containing the branch key +- the base schema is non-nullable +- each branch is a top-level, non-nullable `@AckType` object schema in the same library +- branch schemas omit the discriminator field, or include it as `Ack.literal(...)` matching the branch key, or `Ack.enumString(...)` containing the branch key Example: @@ -92,10 +82,7 @@ final petSchema = Ack.discriminated( ); ``` -`Ack.discriminated(...)` owns the discriminator property. Boundary payloads -must still include the discriminator key, but branch schemas should usually -omit it. If a branch schema includes the discriminator field, it must be -an exact literal or enum containing the branch map key: +`Ack.discriminated(...)` owns the discriminator property. Boundary payloads must include the discriminator key; branch schemas should usually omit it. When a branch includes the discriminator field, it must be an exact literal or enum containing the branch key: ```dart @AckType() @@ -105,33 +92,32 @@ final catSchema = Ack.object({ }); ``` -Conflicting discriminator fields are rejected. Exported and generated schemas -treat the discriminator as an exact literal for each branch. Broad -`Ack.string()`, transformed/refined discriminator fields, and restrictive -chains are rejected. Generated subtype `parse()` / `safeParse()` methods -validate through the union's effective branch. +Conflicting discriminator fields, broad `Ack.string()`, and transformed or refined discriminator fields are rejected. Generated subtype `parse()` / `safeParse()` methods validate through the union's effective branch. ## Resolution rules -`@AckType()` uses strict schema resolution. - -- Nested object fields must reference a named top-level schema. -- Inline anonymous object fields are rejected for typed generation. -- `Ack.list(...)` elements must be statically resolvable. +- Nested object fields must reference a named top-level schema — inline anonymous objects are rejected. +- `Ack.list(...)` element schemas must be statically resolvable. - Cross-file references work for direct imports, prefixed imports, and re-exports. -- Unannotated object schema references fail generation instead of silently falling back to raw maps. +- Unannotated object schema references fail generation rather than silently falling back to raw maps. - Circular alias/reference chains fail generation with a clear error. ## Limitations -- `@AckType()` is only supported on top-level schema variables and getters. +- `@AckType()` only works on top-level schema variables and getters. - Nullable top-level schemas do not emit extension types. - List element nullability may not be preserved in all chained modifier cases. - Use `.transform(...)` with an explicit output type so the generator can infer the representation type. ## Build checklist -1. Add `ack_annotations`, `ack_generator`, and `build_runner`. -2. Add a `part '.g.dart';` directive. -3. Annotate top-level schema variables/getters with `@AckType()`. -4. Run `dart run build_runner build`. +1. Add `ack_annotations`, `ack_generator`, and `build_runner` to your pubspec. +2. Add `part '.g.dart';` to the file. +3. Annotate top-level schema variables or getters with `@AckType()`. +4. Run `dart run build_runner build --delete-conflicting-outputs`. + +## Next steps + +- [JSON Serialization](./json-serialization.mdx) — parse JSON straight into generated types +- [Common Recipes](../guides/common-recipes.mdx) — patterns that combine schemas and generated types +- [API Reference](../api-reference/index.mdx) — the full surface diff --git a/docs/core-concepts/validation.mdx b/docs/core-concepts/validation.mdx index be3f838d..07bb1e7e 100644 --- a/docs/core-concepts/validation.mdx +++ b/docs/core-concepts/validation.mdx @@ -2,24 +2,19 @@ title: Validation Rules --- -Ack provides a rich set of built-in validation rules (constraints) that you can chain onto schema types to enforce specific requirements. Built-in constraints provide default error messages. For custom error messages, use custom constraints (see [Custom Validation Guide](../guides/custom-validation.mdx)). +Constraints turn "a string" into "an email between 2 and 50 characters." Chain them onto any schema — each comes with a sensible default error message, and you can supply your own (see [Custom Validation](../guides/custom-validation.mdx)). -## Common Constraints (Applicable to Multiple Types) +## Common constraints -### `nullable()` +### `nullable()` and `optional()` -Allows the value to be `null` in addition to the type's constraints. - -```dart -final optionalName = Ack.string().nullable(); -final optionalAge = Ack.integer().nullable(); -``` +`.nullable()` lets a present field hold `null`; `.optional()` lets it be omitted. These are field-presence modifiers rather than constraints — see [Optional vs nullable](./schemas.mdx#optional-vs-nullable). ### `constrain(Constraint constraint, {String? message})` Applies a custom `Constraint` that also implements `Validator`. See the [Custom Validation](../guides/custom-validation.mdx) guide. -## String Constraints +## String constraints Apply these to [`Ack.string()`](./schemas.mdx#string) schemas. @@ -35,11 +30,10 @@ Requires at most `max` characters. Ack.string().maxLength(100) ``` -### Exact Length -To ensure a string has exactly a specific length, combine `minLength()` and `maxLength()`: +### `length(int n)` +Requires exactly `n` characters. ```dart -// String must be exactly 10 characters -Ack.string().minLength(10).maxLength(10) +Ack.string().length(10) ``` ### `notEmpty()` @@ -51,7 +45,7 @@ Ack.string().notEmpty() ### `matches(String pattern, {String? example, String? message})` Requires the string to match a regular expression `pattern`. -**Important**: Patterns are NOT automatically anchored. The pattern will match if found anywhere in the string (substring matching). To require full-string matching, explicitly add anchors: `^...$` +Patterns are **not** automatically anchored. The pattern matches if found anywhere in the string. To require a full-string match, add anchors: `^...$` ```dart // Simple alphanumeric pattern (full-string match with anchors) @@ -81,8 +75,6 @@ Requires a valid email address format. Ack.string().email() ``` - - ### `date()` Requires a valid date string in `YYYY-MM-DD` format. ```dart @@ -127,14 +119,14 @@ Ack.string().ipv6() -### `enumString(List allowedValues)` -Requires the value to be one of `allowedValues`. Use this only for ad-hoc string lists. When a Dart enum exists, prefer `Ack.enumValues(MyEnum.values)` for type-safe validation. +### `Ack.enumString(List allowedValues)` +An `Ack` factory (not a fluent method) that creates a `StringSchema` accepting only values in `allowedValues`. Use for ad-hoc string lists; prefer `Ack.enumValues(MyEnum.values)` when a Dart enum exists. ```dart Ack.enumString(['active', 'inactive', 'pending']) ``` -### `literal(String value)` -Requires an exact string match. +### `Ack.literal(String value)` +An `Ack` factory (not a fluent method) that creates a `StringSchema` requiring an exact string match. ```dart Ack.literal('admin') ``` @@ -158,19 +150,19 @@ Ack.string().url() ``` ### `ip({int? version})` -Requires a valid IP address. Set `version` to restrict to IPv4 or IPv6. +Requires a valid IP address. Set `version` to `4` or `6` to restrict the version. ```dart -Ack.string().ip() // Any IP (v4 or v6) +Ack.string().ip() // Any IP (v4 or v6) Ack.string().ip(version: 4) // IPv4 only Ack.string().ip(version: 6) // IPv6 only ``` -## String Transformations +## String transformations -These methods transform the string value during parsing. They don't add validation constraints but modify the output value. +These methods modify the output value during parsing rather than adding a validation constraint. ### `trim()` -Removes leading and trailing whitespace from the string. +Removes leading and trailing whitespace. ```dart Ack.string().trim() // " hello " → "hello" @@ -190,7 +182,7 @@ Ack.string().toUpperCase() // "hello" → "HELLO" ``` -## Number Constraints +## Number constraints Apply these to [`Ack.integer()`](./schemas.mdx#number), [`Ack.double()`](./schemas.mdx#number), or [`Ack.number()`](./schemas.mdx#number) schemas. @@ -250,7 +242,7 @@ Ack.double().negative() // < 0.0 Ack.number().negative() // < 0 ``` -### `safe()` (Integer only) +### `safe()` (integer only) Requires an integer within JavaScript's safe range (`-2^53+1` to `2^53-1`). ```dart Ack.integer().safe() @@ -258,14 +250,14 @@ Ack.integer().safe() ### `finite()` Requires a finite number (rejects `infinity` and `NaN`). `Ack.double()` and -`Ack.number()` already enforce this by default; the method is available for +`Ack.number()` enforce this by default; `finite()` is available for explicitness and API symmetry. ```dart Ack.double().finite() Ack.number().finite() ``` -## List Constraints +## List constraints Apply these to [`Ack.list()`](./schemas.mdx#list) schemas. @@ -294,14 +286,14 @@ Ack.list(Ack.object({})).notEmpty() ``` ### `unique()` -Requires all items to be unique. Uses deep structural equality, so nested maps/lists are compared by value. +Requires all items to be unique. Uses deep structural equality, so nested maps and lists are compared by value. ```dart Ack.list(Ack.string()).unique() ``` -## Primitive Type Strictness +## Primitive type strictness -Primitive schemas (`Ack.string()`, `Ack.integer()`, `Ack.double()`, `Ack.number()`, `Ack.boolean()`) are strict: a value must already match the expected Dart runtime type. Mismatched inputs surface as a `TypeMismatchError` instead of being silently coerced. +Primitive schemas (`Ack.string()`, `Ack.integer()`, `Ack.double()`, `Ack.number()`, `Ack.boolean()`) are strict: a value must already match the expected Dart runtime type. Mismatched inputs surface as a `TypeMismatchError` rather than being silently coerced. Each schema maps to a specific runtime type: @@ -334,9 +326,9 @@ numberSchema.safeParse(3.14); // ✅ OK (double is num) numberSchema.safeParse('42'); // ❌ FAIL: TypeMismatchError ``` -Because `Ack.integer()` and `Ack.double()` do not overlap, use `Ack.number()` when a field may legitimately be either an int or a double. Reach for `transform`/`codec` only when the boundary value isn't already a `num` (for example, a numeric string). +Because `Ack.integer()` and `Ack.double()` do not overlap, use `Ack.number()` when a field may be either an `int` or a `double`. Use `transform`/`codec` only when the boundary value isn't already a `num` (for example, a numeric string). -This strictness makes `anyOf` and discriminated unions reliable — they can distinguish, for example, the string `"42"` from the integer `42` without configuration: +This strictness makes `anyOf` and discriminated unions reliable — they can distinguish the string `"42"` from the integer `42` without configuration: ```dart final stringOrNumber = Ack.anyOf([ @@ -348,14 +340,13 @@ stringOrNumber.safeParse('42'); // ✅ Matches string branch stringOrNumber.safeParse(42); // ✅ Matches integer branch ``` -### Converting Boundary Types +### Converting boundary types -When your boundary payload uses a different shape from your runtime model (for example, ISO strings → `DateTime`, or `"true"`/`"false"` → `bool`), express the conversion explicitly with [`transform`](./schemas.mdx#transformations) or a `codec`: +When your boundary payload uses a different shape than your runtime model (for example, ISO strings → `DateTime`, or `"true"`/`"false"` → `bool`), express the conversion explicitly with [`transform`](./schemas.mdx#transformations) or a `codec`: ```dart // Boundary "true"/"false" string → runtime bool -final boolFromString = Ack.string() - .enumString(['true', 'false']) +final boolFromString = Ack.enumString(['true', 'false']) .transform((s) => s == 'true'); boolFromString.safeParse('true'); // ✅ runtime value: true @@ -365,29 +356,27 @@ boolFromString.safeParse(true); // ❌ FAIL: string schema rejects bool Use `schema.codec(decode: ..., encode: ...)` when you also need a reversible encode path back to the boundary type. -## Combining Constraints +## Combining constraints -You can chain multiple constraints together. Ack evaluates them in the order you apply them. +You can chain multiple constraints. Ack evaluates them in the order you apply them. ```dart final usernameSchema = Ack.string() - .minLength(3) // First: check min length - .maxLength(20) // Second: check max length + .minLength(3) // First: check min length + .maxLength(20) // Second: check max length .matches(r'[a-z0-9_]+') // Third: check pattern (lowercase alphanumeric/underscore) - .notEmpty(); // Redundant if minLength(>0) is used, but illustrates chaining + .notEmpty(); // Redundant if minLength(>0) is used, but illustrates chaining final quantitySchema = Ack.integer() - .min(1) // Must be at least 1 - .max(100) // Must be at most 100 - .multipleOf(1); // Must be an integer (redundant for Ack.integer) + .min(1) // Must be at least 1 + .max(100) // Must be at most 100 + .multipleOf(1); // Must be an integer (redundant for Ack.integer) ``` -## Next Steps - -Now that you understand validation rules, explore these related topics: +## Next steps -- **[Error Handling](/core-concepts/error-handling)**: Learn how to handle and display validation errors -- **[Custom Validation](/guides/custom-validation)**: Create custom constraints and refinement logic -- **[Schema Types](/core-concepts/schemas)**: Explore all available schema types and their operations -- **[Common Recipes](/guides/common-recipes)**: See practical validation patterns and solutions -- **[Flutter Forms](/guides/flutter-form-validation)**: Integrate validation with Flutter form widgets +- **[Error handling](/core-concepts/error-handling)**: Handle and display validation errors +- **[Custom validation](/guides/custom-validation)**: Create custom constraints and refinement logic +- **[Schema types](/core-concepts/schemas)**: All available schema types and their operations +- **[Common recipes](/guides/common-recipes)**: Practical validation patterns and solutions +- **[Flutter forms](/guides/flutter-form-validation)**: Integrate validation with Flutter form widgets diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx index 328509b0..b25f6a0c 100644 --- a/docs/getting-started/installation.mdx +++ b/docs/getting-started/installation.mdx @@ -3,7 +3,7 @@ title: Installing Ack --- -## Add to Your Project +## Add to your project Add Ack to your project using the Dart CLI: @@ -15,17 +15,16 @@ dart pub add ack flutter pub add ack ``` -Or manually add to your `pubspec.yaml` (check [pub.dev](https://pub.dev/packages/ack) for the latest version): +Or add to your `pubspec.yaml` (check [pub.dev](https://pub.dev/packages/ack) for the latest version): ```yaml dependencies: ack: ^1.0.0 # Replace with latest version ``` -## Code Generator (`@AckType()`) +## Code generator (`@AckType()`) -If you want typed wrappers for hand-written schemas, add the annotation and -generator packages alongside `ack`: +To generate typed wrappers for hand-written schemas, add the annotation and generator packages alongside `ack`: ```bash dart pub add ack ack_annotations @@ -44,9 +43,7 @@ dev_dependencies: build_runner: ^2.4.0 ``` -`ack_generator` does not generate schemas from classes. It reads top-level Ack -schema variables and getters annotated with `@AckType()` and emits typed -extension wrappers with `parse()` / `safeParse()` helpers. +`ack_generator` does not generate schemas from classes. It reads top-level Ack schema variables and getters annotated with `@AckType()` and emits typed extension wrappers with `parse()`/`safeParse()` helpers. ```dart import 'package:ack/ack.dart'; @@ -61,37 +58,32 @@ final userSchema = Ack.object({ }); ``` -Run the generator with: +Run the generator: ```bash dart run build_runner build --delete-conflicting-outputs ``` -See the [generator documentation](https://docs.page/btwld/ack) for more -AckType examples and supported schema shapes. +See the [generator documentation](https://docs.page/btwld/ack) for more `@AckType` examples and supported schema shapes. -## Basic Usage after Installation +## Basic usage Import Ack in your Dart files: ```dart import 'package:ack/ack.dart'; -// 1. Define a simple schema (e.g., for a name) final nameSchema = Ack.string().minLength(3); - -// 2. Validate some data against the schema final result = nameSchema.safeParse('John'); -// 3. Check the result if (result.isOk) { - print('Valid: ${result.getOrThrow()}'); // Access the valid data + print('Valid: ${result.getOrThrow()}'); } else { - print('Invalid: ${result.getError()}'); // Get the error details + print('Invalid: ${result.getError()}'); } ``` -See the [Quickstart Tutorial](./quickstart-tutorial.mdx) for a more complete example. +See the [Quickstart Tutorial](./quickstart-tutorial.mdx) for a complete example. ## Requirements diff --git a/docs/getting-started/quickstart-tutorial.mdx b/docs/getting-started/quickstart-tutorial.mdx index 8ce7f20b..2e3f1399 100644 --- a/docs/getting-started/quickstart-tutorial.mdx +++ b/docs/getting-started/quickstart-tutorial.mdx @@ -2,158 +2,78 @@ title: Quickstart Tutorial --- -This tutorial guides you through the basics of using Ack to define a schema, validate data, and handle the results. +This tutorial validates a Dart map — the kind you get from `jsonDecode()` of an API response or a form submission — and handles every outcome. ## Prerequisites -Make sure you have [installed Ack](./installation.mdx) in your Dart or Flutter project. - -## Steps - -1. **Import Ack:** - Start by importing the Ack library in your Dart file. - - ```dart - import 'package:ack/ack.dart'; - ``` - -2. **Define a Schema:** - Let's create a schema for a simple user object with a name (required string) and age (optional integer). - - ```dart - final userSchema = Ack.object({ - // 'name' field: must be a string with at least 2 characters. - 'name': Ack.string().minLength(2), - // 'age' field: must be an integer, 0 or greater, but can be missing (optional). - 'age': Ack.integer().min(0).optional(), - }); - ``` - - *Learn more about [Schemas](../core-concepts/schemas.mdx) and [Validation](../core-concepts/validation.mdx).* - -3. **Prepare Data:** - Create some data (usually a `Map`) that you want to validate. - - ```dart - // Example 1: Valid data - final validData = { - 'name': 'Alice', - 'age': 30, - }; - - // Example 2: Valid data (age is optional) - final validDataNoAge = { - 'name': 'Bob', - }; - - // Example 3: Invalid data (name too short) - final invalidDataShortName = { - 'name': 'X', - 'age': 25, - }; - - // Example 4: Invalid data (name is missing) - final invalidDataMissingName = { - 'age': 40, - }; - ``` - -4. **Validate Data:** - Use the `safeParse()` method of your schema to check the data. This returns a `SchemaResult` object. - - ```dart - final result1 = userSchema.safeParse(validData); - final result2 = userSchema.safeParse(validDataNoAge); - final result3 = userSchema.safeParse(invalidDataShortName); - final result4 = userSchema.safeParse(invalidDataMissingName); - ``` - -5. **Handle the Result:** - Check the `isOk` property of the `SchemaResult`. If `true`, validation passed. If `false`, validation failed. - - ```dart - void checkResult(result, dynamic originalData) { - print('\nChecking: $originalData'); - if (result.isOk) { - // Validation succeeded! - // Safely get the validated data (type matches schema structure) - final validatedData = result.getOrThrow(); - print(' Result: OK'); - print(' Validated Data: $validatedData'); - // You can now confidently use validatedData - // e.g., String name = validatedData['name']; - // int? age = validatedData['age']; - } else { - // Validation failed! - // Get the specific error details - final error = result.getError(); - print(' Result: FAILED'); - print(' Error Name: ${error.name}'); - print(' Error Details: $error'); - } - } - - // Run checks on our examples - checkResult(result1, validData); - checkResult(result2, validDataNoAge); - checkResult(result3, invalidDataShortName); - checkResult(result4, invalidDataMissingName); - ``` - - *Learn more about [Error Handling](../core-concepts/error-handling.mdx).* - -## Full Example Code +[Install Ack](./installation.mdx) in your Dart or Flutter project. + +## 1. Define a schema + +Describe the shape you expect. Fields are required unless you mark them `.optional()`. + +```dart +import 'package:ack/ack.dart'; + +final userSchema = Ack.object({ + 'name': Ack.string().minLength(2), // required, min 2 chars + 'age': Ack.integer().min(0).optional(), // optional, non-negative +}); +``` + +See [Schema Types](../core-concepts/schemas.mdx) and [Validation Rules](../core-concepts/validation.mdx) for everything you can express. + +## 2. Validate data + +Call `safeParse()` with the data you received — for example `jsonDecode(response.body)`. It returns a `SchemaResult` and never throws. + +```dart +final result = userSchema.safeParse({'name': 'Alice', 'age': 30}); + +if (result.isOk) { + final user = result.getOrThrow(); // validated Map + print('Valid: $user'); +} else { + print('Invalid: ${result.getError()}'); // structured error with a path +} +``` + +Prefer exceptions? Use `parse()` instead — it returns the value or throws `AckException`. + +## 3. Handle every outcome + +A schema accepts valid data and rejects each way it can be wrong. This runnable example checks four inputs: ```dart import 'package:ack/ack.dart'; void main() { - // Define Schema final userSchema = Ack.object({ 'name': Ack.string().minLength(2), 'age': Ack.integer().min(0).optional(), }); - // Data Examples - final validData = {'name': 'Alice', 'age': 30}; - final validDataNoAge = {'name': 'Bob'}; - final invalidDataShortName = {'name': 'X', 'age': 25}; - final invalidDataMissingName = {'age': 40}; - - // Validate Data - final result1 = userSchema.safeParse(validData); - final result2 = userSchema.safeParse(validDataNoAge); - final result3 = userSchema.safeParse(invalidDataShortName); - final result4 = userSchema.safeParse(invalidDataMissingName); - - // Handle Results - checkResult(result1, validData); - checkResult(result2, validDataNoAge); - checkResult(result3, invalidDataShortName); - checkResult(result4, invalidDataMissingName); + checkResult(userSchema.safeParse({'name': 'Alice', 'age': 30})); // OK + checkResult(userSchema.safeParse({'name': 'Bob'})); // OK: age omitted + checkResult(userSchema.safeParse({'name': 'X', 'age': 25})); // name too short + checkResult(userSchema.safeParse({'age': 40})); // name missing } -// Helper function to print results -void checkResult(result, dynamic originalData) { - print('\nChecking: $originalData'); +void checkResult(SchemaResult result) { if (result.isOk) { - final validatedData = result.getOrThrow(); - print(' Result: OK'); - print(' Validated Data: $validatedData'); + print('OK: ${result.getOrThrow()}'); } else { final error = result.getError(); - print(' Result: FAILED'); - print(' Error Name: ${error.name}'); - print(' Error Details: $error'); + print('FAILED at ${error.path}: ${error.message}'); // path is a JSON Pointer } } ``` -## Next Steps +See [Error Handling](../core-concepts/error-handling.mdx) to read and display errors. -Now that you understand the basics, explore more advanced topics: +## Next steps -- Dive deeper into different [Schema Types](../core-concepts/schemas.mdx). -- Learn about [Built-in Validation Rules](../core-concepts/validation.mdx). -- Discover how to add [Custom Validation](../guides/custom-validation.mdx). -- See how Ack integrates with [JSON Serialization](../core-concepts/json-serialization.mdx). \ No newline at end of file +- [Schema Types](../core-concepts/schemas.mdx) — all available schema types +- [Validation Rules](../core-concepts/validation.mdx) — built-in constraints +- [Custom Validation](../guides/custom-validation.mdx) — add your own logic +- [JSON Serialization](../core-concepts/json-serialization.mdx) — parse and encode JSON diff --git a/docs/guides/common-recipes.mdx b/docs/guides/common-recipes.mdx index 60c3d3c0..f4253b68 100644 --- a/docs/guides/common-recipes.mdx +++ b/docs/guides/common-recipes.mdx @@ -2,9 +2,9 @@ title: Common Recipes --- -This page provides quick solutions to common validation scenarios. Each recipe shows a complete, working example you can copy and adapt. +Quick solutions to common validation scenarios. Each recipe is a complete, working example you can copy and adapt. -## Email and Password Validation +## Email and password validation Validate user credentials with proper constraints: @@ -42,7 +42,7 @@ if (result.isOk) { } ``` -## Nested Object Validation +## Nested object validation Validate nested data structures like addresses: @@ -74,7 +74,7 @@ final result = userWithAddressSchema.safeParse({ }); ``` -## List Validation +## List validation Validate lists with constraints on items and length: @@ -97,7 +97,7 @@ final postSchema = Ack.object({ }); ``` -## Enum Validation +## Enum validation Validate values against a set of allowed options using Dart enums: @@ -119,7 +119,7 @@ final result = orderSchema.safeParse({ }); ``` -## Custom Validation +## Custom validation Create reusable custom validators for domain-specific rules: @@ -156,7 +156,7 @@ final registrationSchema = Ack.object({ ); ``` -## API Response Validation +## API response validation Validate external API responses before using the payload: @@ -193,9 +193,9 @@ Future fetchUser(String username) async { } ``` -## See Also +## See also -- [Custom Validation Guide](./custom-validation.mdx) - Deep dive into custom validators -- [Flutter Form Validation](./flutter-form-validation.mdx) - Integrate with Flutter forms -- [Schema Types](../core-concepts/schemas.mdx) - All available schema types -- [Validation Rules](../core-concepts/validation.mdx) - Complete constraint reference +- [Custom validation guide](./custom-validation.mdx) — deep dive into custom validators +- [Flutter form validation](./flutter-form-validation.mdx) — integrate with Flutter forms +- [Schema types](../core-concepts/schemas.mdx) — all available schema types +- [Validation rules](../core-concepts/validation.mdx) — complete constraint reference diff --git a/docs/guides/creating-schema-converter-packages.md b/docs/guides/creating-schema-converter-packages.md index 5a166d88..4aec863a 100644 --- a/docs/guides/creating-schema-converter-packages.md +++ b/docs/guides/creating-schema-converter-packages.md @@ -330,6 +330,12 @@ class SchemaConverter { AckOneOfSchemaModel() => _convertOneOf(schema), AckAllOfSchemaModel() => _convertAllOf(schema), AckNullSchemaModel() => _buildNullSchema(), + // Lazy/recursive schemas (Ack.lazy) surface as references by name. + // Map schema.refName to your target's reference mechanism, or throw if + // the target cannot express recursion. + AckRefSchemaModel() => throw UnsupportedError( + 'Reference schemas (Ack.lazy) are not supported by this target.', + ), }; } @@ -1181,6 +1187,7 @@ TargetSchema _convert(AckSchemaModel schema) { AckOneOfSchemaModel() => _convertOneOf(schema), AckAllOfSchemaModel() => _convertAllOf(schema), AckNullSchemaModel() => TargetSchema.nullValue(), + AckRefSchemaModel() => TargetSchema.ref(schema.refName), }; } diff --git a/docs/guides/custom-validation.mdx b/docs/guides/custom-validation.mdx index 499659d2..9b9802fc 100644 --- a/docs/guides/custom-validation.mdx +++ b/docs/guides/custom-validation.mdx @@ -6,13 +6,11 @@ While Ack provides many [built-in validation rules](../core-concepts/validation. ## Prerequisites -Before creating custom validation, you should understand: - - **[Validation Rules](/core-concepts/validation)**: Built-in constraints and how they work - **[Schema Types](/core-concepts/schemas)**: Different schema types and their behavior - **[Error Handling](/core-concepts/error-handling)**: How validation errors are structured -## Creating a Value Constraint +## Creating a value constraint To add reusable validation that only depends on the field value, implement a `Constraint` that mixes in `Validator`. @@ -39,7 +37,7 @@ print(priceSchema.safeParse(10.5).isOk); // true print(priceSchema.safeParse(-5).isFail); // true ``` -## Cross-Field Rules with `.refine()` +## Cross-field rules with `.refine()` When validation depends on multiple fields, use `.refine()` on the parent object schema. @@ -59,7 +57,7 @@ final result = signUpSchema.safeParse({ print(result.isFail); // true ``` -## Overriding Error Messages +## Overriding error messages The optional `message` parameter on `.constrain()` lets you customize the failure message. @@ -67,10 +65,13 @@ The optional `message` parameter on `.constrain()` lets you customize the failur final schema = Ack.double() .constrain(IsPositiveConstraint(), message: 'Price must be greater than zero.'); -print(schema.safeParse(-10).getError().toString()); // Price must be greater than zero. +final result = schema.safeParse(-10); +if (result.isFail) { + print(result.getError().message); // Price must be greater than zero. +} ``` -## Organizing Reusable Constraints +## Organizing reusable constraints Place frequently used constraints in utility files so they can be shared across schemas. @@ -92,11 +93,17 @@ final userSchema = Ack.object({ }); ``` -## When to Use Custom Logic +## When to use custom logic + +- **Complex business rules:** Domain-specific checks that built-ins don’t cover. +- **Cross-field relationships:** Comparing password and confirmation fields, for example. +- **Reusable patterns:** Common rules applied across multiple schemas. +- **External service checks:** Validating against APIs or databases (beware of latency). + +Chaining built-in constraints is often enough for simple cases. Use custom constraints or refinements when you need extra flexibility. -- **Complex Business Rules:** Domain-specific checks that built-ins don’t cover. -- **Cross-Field Relationships:** e.g. comparing password and confirmation fields. -- **Reusable Patterns:** Common rules you apply across multiple schemas. -- **External Service Checks:** Validating against APIs or databases (beware of performance/latency). +## Next steps -For simple cases, chaining built-in constraints is often enough. Reach for custom constraints/refinements when you need extra flexibility. +- [Common Recipes](./common-recipes.mdx) — custom constraints in real-world schemas +- [Flutter Form Validation](./flutter-form-validation.mdx) — surface custom messages in forms +- [Error Handling](../core-concepts/error-handling.mdx) — read `SchemaConstraintsError` diff --git a/docs/guides/flutter-form-validation.mdx b/docs/guides/flutter-form-validation.mdx index 331dd47e..d7052fca 100644 --- a/docs/guides/flutter-form-validation.mdx +++ b/docs/guides/flutter-form-validation.mdx @@ -6,19 +6,17 @@ This guide shows how to use Ack for validating forms in Flutter applications. ## Prerequisites -Before following this guide, you should be familiar with: - - **Flutter basics**: Creating widgets, managing state, and using forms - **Ack fundamentals**: Creating schemas and validation (see [Quickstart Tutorial](/getting-started/quickstart-tutorial)) - **Validation rules**: Built-in constraints (see [Validation Rules](/core-concepts/validation)) -You should have Ack installed in your Flutter project: +Install Ack in your Flutter project: ```bash flutter pub add ack ``` -## Basic Form Validation with `TextFormField` +## Basic form validation with `TextFormField` Ack works with Flutter form APIs by running `safeParse` inside `TextFormField.validator` and returning a `String?` error message @@ -148,17 +146,11 @@ class _SignUpFormState extends State { } ``` -**Key Points:** - -1. **Define Schemas:** Create `AckSchema` instances for each form field using rules from [Validation Rules](../core-concepts/validation.mdx). -2. **`TextFormField.validator`:** Inside the `validator` function, call `schema.safeParse(value)`. If `result.isFail`, return `result.getError().message` to display the error (see [Error Handling](../core-concepts/error-handling.mdx)). -3. **`GlobalKey`:** Use a key to manage the form and trigger validation on submission via `_formKey.currentState!.validate()`. -4. **`AutovalidateMode`:** Set `autovalidateMode` (e.g., `AutovalidateMode.onUserInteraction`) on `TextFormField` for real-time feedback as the user types or interacts. -5. **Custom Error Messages:** Provide custom error messages directly within the schema definition using the `message:` parameter (see [Error Handling](../core-concepts/error-handling.mdx#custom-error-messages)). +Key steps: define an `AckSchema` per field; in `TextFormField.validator`, call `schema.safeParse(value)` and return `result.getError().toString()` on failure; trigger full-form validation via `_formKey.currentState!.validate()`; set `autovalidateMode: AutovalidateMode.onUserInteraction` for real-time feedback. Pass `message:` inside the schema definition for [custom error messages](../core-concepts/error-handling.mdx#custom-error-messages). -## Real-time Validation with `TextField` +## Real-time validation with `TextField` -If you are not using a `Form` widget or prefer manual state management for errors, you can listen to controller changes and update the error state directly. +Without a `Form` widget, listen to controller changes and update the error state directly. ```dart import 'package:ack/ack.dart'; @@ -230,17 +222,11 @@ class _RealtimeValidationFieldState extends State { } ``` -**Key Points:** +Key steps: hold a state variable for the error message; add a listener to `TextEditingController` in `initState`; call `schema.safeParse()` inside the listener and `setState` with the result; remove the listener in `dispose`. -1. **State Variable:** Maintain a state variable (e.g., `_emailErrorText`) to hold the current error message for the field. -2. **Controller Listener:** Add a listener to the `TextEditingController` in `initState`. -3. **Validation Logic:** Inside the listener, call `schema.safeParse()` with the controller's text. -4. **`setState`:** Call `setState` to update the error state variable based on the validation result. This triggers a UI rebuild to show/hide the error. -5. **Dispose:** Remember to remove the listener in `dispose`. +## Validating entire forms on submission -## Validating Entire Models/Forms on Submission - -Instead of validating field by field, you can validate a complete data structure (like a map representing the whole form) on submission using [`Ack.object`](../core-concepts/schemas.mdx#object). +Instead of validating field by field, validate the complete data structure on submission using [`Ack.object`](../core-concepts/schemas.mdx#object). ```dart // Define a schema for the whole form data @@ -279,10 +265,4 @@ void _submitForm() { } ``` -**Key Points:** - -1. **Object Schema:** Create an [`Ack.object`](../core-concepts/schemas.mdx#object) schema representing the entire form's data structure. -2. **Reuse Schemas:** Reuse the individual field schemas within the object schema. -3. **Cross-Field Validation:** Use object-level [`.refine()`](./custom-validation.mdx#cross-field-rules-with-refine) for rules that depend on multiple fields. -4. **Centralized Validation:** Validate the complete `formData` map against the `_formSchema` on submission. -5. **Error Handling:** If validation fails, you might need logic to map the [`error.path`](../core-concepts/error-handling.mdx#understanding-schemaerror) back to the specific UI field(s) causing the error, especially if not using `TextFormField`'s built-in validation display. +Key steps: build an [`Ack.object`](../core-concepts/schemas.mdx#object) schema that reuses your field schemas; add [`.refine()`](./custom-validation.mdx#cross-field-rules-with-refine) for cross-field rules; call `safeParse` on the full form map at submission. On failure, use [`error.path`](../core-concepts/error-handling.mdx#understanding-schemaerror) to map errors back to specific fields when not using `TextFormField`'s built-in display. diff --git a/docs/guides/json-schema-integration.mdx b/docs/guides/json-schema-integration.mdx index d7297586..83814795 100644 --- a/docs/guides/json-schema-integration.mdx +++ b/docs/guides/json-schema-integration.mdx @@ -2,18 +2,16 @@ title: JSON Schema Integration --- -Ack [schemas](../core-concepts/schemas.mdx) can be automatically converted into JSON Schema objects, allowing you to generate API documentation directly from your validation schemas. +Ack [schemas](../core-concepts/schemas.mdx) can be converted into JSON Schema objects, letting you generate API documentation directly from your validation schemas. -Ack also exposes `AckSchemaModel`, the canonical export model used by schema -adapter packages. Use `schema.toSchemaModel()` when you are building a -converter to another target format; JSON Schema is one renderer of that model, -not the source of truth for adapter behavior. +> **Building an adapter package?** Most users only need `toJsonSchema()`. If you +> convert Ack schemas to another target format, render from +> `schema.toSchemaModel()` (the canonical `AckSchemaModel`) rather than the JSON +> Schema map — see [Creating Schema Converters](./creating-schema-converter-packages.md). -## Generating JSON Schemas +## Generating JSON schemas -Use the `toJsonSchema()` method available on any `AckSchema` instance. This is -the same generic Draft-7 renderer used by -`schema.toSchemaModel().toJsonSchema()`. +Call `toJsonSchema()` on any `AckSchema` instance. This is the same generic Draft-7 renderer used by `schema.toSchemaModel().toJsonSchema()`. ```dart import 'package:ack/ack.dart'; @@ -40,10 +38,9 @@ void main() { } ``` -## Adapter Model +## Adapter model -Use `toSchemaModel()` when you need a reusable, target-independent view of an -Ack schema: +Use `toSchemaModel()` for a reusable, target-independent view of an Ack schema: ```dart final model = userSchema.toSchemaModel(); @@ -54,10 +51,7 @@ for (final warning in model.warnings) { } ``` -`AckSchemaModel` describes the boundary values a schema accepts and exports. It -keeps adapter metadata such as object property ordering and discriminator -metadata. Its JSON Schema renderer emits only generic Draft-7-compatible -output; provider-specific hints belong in adapter renderers. +`AckSchemaModel` describes the boundary values a schema accepts and exports, keeping adapter metadata such as property ordering and discriminator info. Its JSON Schema renderer emits only generic Draft-7-compatible output; provider-specific hints belong in adapter renderers. **Output JSON (JSON Schema Object):** @@ -137,9 +131,9 @@ output; provider-specific hints belong in adapter renderers. } ``` -## How Constraints Map to JSON Schema +## How constraints map to JSON Schema -Ack attempts to map its [built-in constraints](../core-concepts/validation.mdx) to corresponding JSON Schema keywords: +Ack maps its [built-in constraints](../core-concepts/validation.mdx) to corresponding JSON Schema keywords: | Ack Constraint or Schema | JSON Schema Keyword | Notes | | :----------------------- | :------------------ | :---- | @@ -154,7 +148,7 @@ Ack attempts to map its [built-in constraints](../core-concepts/validation.mdx) | [`uuid()`](../core-concepts/validation.mdx#string-constraints) | `format: uuid` | String | | [`ipv4()`](../core-concepts/validation.mdx#string-constraints) | `format: ipv4` | String | | [`ipv6()`](../core-concepts/validation.mdx#string-constraints) | `format: ipv6` | String | -| [`enumString([...])`](../core-concepts/validation.mdx#string-constraints) | `enum: [...]` | String | +| [`Ack.enumString([...])`](../core-concepts/validation.mdx#string-constraints) | `enum: [...]` | String | | [`min(n)`](../core-concepts/validation.mdx#number-constraints) | `minimum: n` | Number | | [`max(n)`](../core-concepts/validation.mdx#number-constraints) | `maximum: n` | Number | | [`greaterThan(n)`](../core-concepts/validation.mdx#number-constraints) | `exclusiveMinimum: n` | Number (exclusive) | @@ -163,7 +157,7 @@ Ack attempts to map its [built-in constraints](../core-concepts/validation.mdx) | [`minLength(n)`](../core-concepts/validation.mdx#list-constraints) | `minItems: n` | List (array) | | [`maxLength(n)`](../core-concepts/validation.mdx#list-constraints) | `maxItems: n` | List (array) | | [`unique()`](../core-concepts/validation.mdx#list-constraints) | `uniqueItems: true` | List (array) | -| [`nullable()`](../core-concepts/validation.mdx#nullable) | `anyOf: [, {type: null}]` | Any schema | +| [`nullable()`](../core-concepts/schemas.mdx#optional-vs-nullable) | `anyOf: [, {type: null}]` | Any schema | | `withDefault(v)` | `default: v` | JSON/export-safe defaults only in `AckSchemaModel`; unsupported defaults are omitted with a warning | | `describe(d)` | `description: d` | Any schema | | [`Ack.integer()`](../core-concepts/schemas.mdx#number) | `type: integer` | Type | @@ -174,10 +168,9 @@ Ack attempts to map its [built-in constraints](../core-concepts/validation.mdx) | [`Ack.object(...)`](../core-concepts/schemas.mdx#object) | `type: object`, `properties: {...}`, `required: [...]` | Type | | [`Ack.lazy(...)`](../core-concepts/schemas.mdx#recursive-schemas) | `definitions`, `$ref` | Recursive type | -## Shape Stability Notes +## Shape stability notes -`toJsonSchema()` renders generic Draft-7 JSON Schema and uses stable -nullability rules: +`toJsonSchema()` renders generic Draft-7 JSON Schema with stable nullability rules: - Primitive/object/list/enum schemas marked with `.nullable()` are emitted as: - `anyOf: [, { "type": "null" }]` @@ -215,14 +208,9 @@ And nullable discriminated unions are represented as: } ``` -If you build consumers that inspect generated schemas, treat nullability and -union composition as separate concerns and avoid assuming enum values always -live at the top level. +If you build consumers that inspect generated schemas, treat nullability and union composition as separate concerns and don't assume enum values always live at the top level. -The nested nullable-union shape is intentional for generic Draft-7 output and -matches Zod v4's `toJSONSchema()` renderer. Do not flatten it in -`AckSchema.toJsonSchema()`; provider-specific adapters that require a different -shape should implement that as explicit adapter rendering. +The nested nullable-union shape is intentional for generic Draft-7 output and matches Zod v4's `toJSONSchema()` renderer. Don't flatten it in `AckSchema.toJsonSchema()`; provider-specific adapters that need a different shape should implement explicit adapter rendering. **Limitations:** @@ -250,9 +238,9 @@ shape should implement that as explicit adapter rendering. `Ack.enumString(...)` fields are accepted; generated branches expose the exact branch value as `const`. -## Integrating into API Documentation +## Integrating into API documentation -You can use the generated JSON Schema map within a larger API documentation structure. +Use the generated JSON Schema map within a larger API documentation structure. ```dart // Assume you have a function to build the full API spec @@ -289,11 +277,11 @@ final fullApiSpec = buildApiSpecification(); print(JsonEncoder.withIndent(' ').convert(fullApiSpec)); ``` -This allows you to maintain your validation logic and API documentation source in one place (your Ack schemas). +This keeps your validation logic and API documentation in one place. -## Advanced JSON Schema Features +## Advanced JSON Schema features -### Schema Descriptions and Metadata +### Schema descriptions and metadata Add descriptions and metadata to your schemas for better documentation: @@ -306,12 +294,12 @@ final userSchema = Ack.object({ }).describe('Represents a user in the system'); final jsonSchema = userSchema.toJsonSchema(); -// Generated schema will include description fields +// Output includes description fields ``` -### Default Values in JSON Schema +### Default values in JSON Schema -Schemas with default values will include them in the generated JSON Schema: +Schemas with default values include them in the generated JSON Schema: ```dart final configSchema = Ack.object({ @@ -321,10 +309,10 @@ final configSchema = Ack.object({ }); final jsonSchema = configSchema.toJsonSchema(); -// Will include "default" properties in the JSON Schema +// Output includes "default" properties ``` -### Complex Schema Patterns +### Complex schema patterns JSON Schema generation works with all Ack schema types: @@ -359,7 +347,7 @@ final complexSchema = Ack.object({ }).optional(), }); -// All generate valid JSON Schema +// All produce valid JSON Schema final mixedJson = mixedValueSchema.toJsonSchema(); final shapeJson = shapeSchema.toJsonSchema(); final complexJson = complexSchema.toJsonSchema(); diff --git a/docs/guides/schema-converter-quickstart.md b/docs/guides/schema-converter-quickstart.md index ce2202f7..c9b7e582 100644 --- a/docs/guides/schema-converter-quickstart.md +++ b/docs/guides/schema-converter-quickstart.md @@ -111,6 +111,9 @@ class SchemaConverter { AckOneOfSchemaModel() => _convertOneOf(schema), AckAllOfSchemaModel() => _convertAllOf(schema), AckNullSchemaModel() => {'type': 'null'}, + AckRefSchemaModel(refName: final name) => { + r'$ref': '#/definitions/$name', + }, }; } diff --git a/docs/index.mdx b/docs/index.mdx index 1c1f979e..6dddde0e 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -1,98 +1,66 @@ --- title: Overview --- -Ack is a schema validation library for Dart and Flutter that helps you validate data with a simple, fluent API. Ack is short for "acknowledgment". -## LLM Context +External data can't be trusted. APIs change, users mistype, and JSON drifts from what your code expects. Ack is a schema validation library for Dart and Flutter that puts a guardrail at those boundaries: describe the shape you expect once, then check untrusted data against it. You get a clear error when it doesn't match — and your data, validated, when it does. -For AI agents, start with the project index at: - -- [llms.txt](/llms.txt) (redirects to canonical static text) - -## Why Use Ack? - -- **Simplify Validation**: Easily handle complex data validation logic. -- **Validate external payloads**: Enforce required fields, type constraints, - and schema rules at input/output boundaries. -- **Single Source of Truth**: Define data structures and rules in one place. -- **Reduce Boilerplate**: Minimize repetitive validation code. - -## What Ack Does - -- Validates data against defined schemas -- Provides detailed error messages for validation failures -- Generates JSON Schema specifications -- Works directly with JSON data from APIs - -## Quick Start - -Add Ack to your project: - -```bash -dart pub add ack -``` - -### Basic Usage +("Ack" is short for "acknowledgment.") ```dart import 'package:ack/ack.dart'; -// Define the structure and rules for a user object. final userSchema = Ack.object({ - 'name': Ack.string().minLength(2).maxLength(50), // Name must be a string between 2 and 50 chars. - 'age': Ack.integer().min(0).max(120), // Age must be an integer between 0 and 120. - 'email': Ack.string().email().nullable(), // Email must be a valid format, but can be null. -}); // All fields are required by default - -// Data to validate. -final dataToValidate = { - 'name': 'John', - 'age': 30, - 'email': 'john@example.com' -}; - -// Perform the validation against the schema. -final result = userSchema.safeParse(dataToValidate); - -// Check the validation outcome. + 'name': Ack.string().minLength(2).maxLength(50), + 'age': Ack.integer().min(0).max(120), + 'email': Ack.string().email().nullable(), +}); + +final result = userSchema.safeParse({ + 'name': 'Ada', + 'age': 36, + 'email': 'ada@example.com', +}); + if (result.isOk) { - // Validation passed, safely access the validated data. - final validData = result.getOrThrow(); - print('Valid data: $validData'); + print('Welcome, ${result.getOrThrow()!['name']}'); } else { - // Validation failed, access the detailed error. - final error = result.getError(); - print('Validation Error: $error'); // Use error.toString() for full details. + print(result.getError()); // tells you exactly which field failed, and why } ``` -## Core Features +On success, `getOrThrow()` returns your **validated data** as a `Map`. Ack checks and shapes the data — opt into [code generation](/core-concepts/typesafe-schemas) when you'd rather have typed getters than map access. -Ack includes these core capabilities for data validation and transformation: +## Why Ack? -- **[Schema Types](/core-concepts/schemas)**: Define data structures with strings, numbers, booleans, lists, objects, and more -- **[Validation Rules](/core-concepts/validation)**: Apply constraints like length, range, pattern matching, and custom validators -- **[Error Handling](/core-concepts/error-handling)**: Get detailed, structured error messages for validation failures -- **[JSON Serialization](/core-concepts/json-serialization)**: Validate JSON payloads, work with validated data, and add typed wrappers when needed -- **[Configuration](/core-concepts/configuration)**: Schema-level configuration options +- **Guard your boundaries.** Catch bad API responses, user input, and config before they reach your logic. +- **Define each shape once.** Reuse one schema for validation, JSON Schema export, and typed models. +- **Errors you can act on.** Every failure points at the exact field and the reason it failed. +- **Just Dart.** A fluent API with no required build step — reach for code generation only when you want it. -## Next Steps +## What do you want to do? -Ready to dive deeper? Start with the [Quickstart Tutorial](/getting-started/quickstart-tutorial) for a step-by-step guide, or explore specific topics: +- **Validate an API response** — [Quickstart Tutorial](/getting-started/quickstart-tutorial), then [JSON Serialization](/core-concepts/json-serialization) +- **Show field errors in a Flutter form** — [Flutter Form Validation](/guides/flutter-form-validation) +- **Make a field optional or nullable** — [Optional vs nullable](/core-concepts/schemas#optional-vs-nullable) +- **Parse dates, URIs, and durations** — [Codecs](/core-concepts/codecs) +- **Add custom validation logic** — [Custom Validation](/guides/custom-validation) +- **Generate typed models (no manual casts)** — [TypeSafe Schemas](/core-concepts/typesafe-schemas) +- **Reuse and compose schemas** — [Common Recipes](/guides/common-recipes) -- [Schema Types](/core-concepts/schemas) - Learn about all available schema types -- [Validation Rules](/core-concepts/validation) - Master validation constraints -- [Error Handling](/core-concepts/error-handling) - Handle validation errors effectively -- [Flutter Integration](/guides/flutter-form-validation) - Validate Flutter forms +## Install + +```bash +dart pub add ack +``` -## Advanced Features +New here? The [Quickstart Tutorial](/getting-started/quickstart-tutorial) takes you from install to handling every validation outcome in a few minutes. -Ack supports these advanced patterns for complex validation scenarios: +## Learn more -- **[Nested validation](/core-concepts/schemas#object)**: Validate deeply nested objects and lists -- **[Custom validation](/guides/custom-validation)**: Add custom logic with `.refine()` and constraints -- **[Union types](/core-concepts/schemas#union-types)**: Handle polymorphic data with `anyOf()` and discriminated unions -- **[Data transformation](/core-concepts/schemas#transformations)**: Transform validated data with `.transform()` -- **[JSON Schema generation](/guides/json-schema-integration)**: Generate JSON Schema specs for API documentation +- [Schema Types](/core-concepts/schemas) — every schema type and how to compose them +- [Validation Rules](/core-concepts/validation) — built-in constraints +- [Error Handling](/core-concepts/error-handling) — read and display structured errors +- [JSON Serialization](/core-concepts/json-serialization) — validate and encode JSON +- [API Reference](/api-reference/) — the full surface -For detailed examples and usage patterns, see the [Common Recipes guide](/guides/common-recipes). +*Building with an AI agent? Start at [`/llms.txt`](/llms.txt) for a compact, machine-readable index.* diff --git a/example/README.md b/example/README.md index 4e769200..0b0d2586 100644 --- a/example/README.md +++ b/example/README.md @@ -12,6 +12,7 @@ This package demonstrates Ack schemas built directly in source and typed with - Edge cases and strict resolution in `lib/schema_types_edge_cases.dart` - Cross-schema object wrappers in `lib/pet.dart`, `lib/user_with_color.dart`, and `lib/args_getter_example.dart` +- Codecs (built-in and custom) in `lib/codecs_example.dart` ## Running the examples diff --git a/example/lib/codecs_example.dart b/example/lib/codecs_example.dart new file mode 100644 index 00000000..26472c41 --- /dev/null +++ b/example/lib/codecs_example.dart @@ -0,0 +1,36 @@ +/// Codec examples for the Ack validation library. +/// +/// Codecs decode boundary (wire) values into rich Dart runtime types and encode +/// them back. Built-in codecs cover common conversions; `Ack.codec(...)` builds +/// a custom bidirectional codec. +library; + +import 'package:ack/ack.dart'; + +/// Priority levels, used with `Ack.enumCodec`. +enum Priority { low, medium, high } + +/// An event whose fields use built-in codecs: +/// +/// - `startsAt`: ISO 8601 string <-> UTC `DateTime` (`Ack.datetime`) +/// - `due`: `YYYY-MM-DD` string <-> local-midnight `DateTime` (`Ack.date`) +/// - `website`: string <-> `Uri` (`Ack.uri`) +/// - `timeout`: milliseconds `int` <-> `Duration` (`Ack.duration`) +/// - `priority`: enum-name string <-> `Priority` (`Ack.enumCodec`) +final eventSchema = Ack.object({ + 'name': Ack.string(), + 'startsAt': Ack.datetime(), + 'due': Ack.date(), + 'website': Ack.uri(), + 'timeout': Ack.duration(), + 'priority': Ack.enumCodec(Priority.values), +}); + +/// A custom bidirectional codec: comma-separated string <-> `List`. +/// +/// `decode` runs on parse; `encode` runs on `schema.encode`. +final tagsCodec = Ack.codec>( + input: Ack.string(), + decode: (value) => value.split(','), + encode: (tags) => tags.join(','), +); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 31d227fc..9ce32d63 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -9,11 +9,11 @@ environment: sdk: '>=3.8.0 <4.0.0' dependencies: - ack: ^1.0.0-beta.8 - ack_annotations: ^1.0.0-beta.11 + ack: ^1.0.0-beta.12 + ack_annotations: ^1.0.0-beta.12 dev_dependencies: - ack_generator: ^1.0.0-beta.8 + ack_generator: ^1.0.0-beta.12 build_runner: ^2.4.0 dart_code_metrics_presets: ^2.22.0 lints: ^5.0.0 diff --git a/example/test/codecs_example_test.dart b/example/test/codecs_example_test.dart new file mode 100644 index 00000000..d4c785a3 --- /dev/null +++ b/example/test/codecs_example_test.dart @@ -0,0 +1,37 @@ +import 'package:ack/ack.dart'; +import 'package:ack_example/codecs_example.dart'; +import 'package:test/test.dart'; + +void main() { + group('Codec examples', () { + test('built-in codecs decode boundary values into Dart runtime types', () { + final event = eventSchema.parse({ + 'name': 'Launch', + 'startsAt': '2026-01-01T09:00:00Z', + 'due': '2026-01-01', + 'website': 'https://example.com', + 'timeout': 5000, + 'priority': 'high', + })!; + + expect(event['startsAt'], isA()); + expect((event['startsAt']! as DateTime).isUtc, isTrue); + expect(event['due'], isA()); + expect(event['website'], isA()); + expect(event['timeout'], const Duration(milliseconds: 5000)); + expect(event['priority'], Priority.high); + }); + + test('custom codec round-trips in both directions', () { + expect(tagsCodec.parse('dart,flutter,ack'), ['dart', 'flutter', 'ack']); + expect(tagsCodec.encode(['dart', 'flutter', 'ack']), 'dart,flutter,ack'); + }); + + test('datetime codec encodes a UTC DateTime back to ISO 8601', () { + expect( + Ack.datetime().encode(DateTime.utc(2026, 1, 1, 9)), + '2026-01-01T09:00:00.000Z', + ); + }); + }); +} diff --git a/llms.txt b/llms.txt index 1a79a56b..b4472d06 100644 --- a/llms.txt +++ b/llms.txt @@ -31,6 +31,20 @@ final result = userSchema.safeParse({ }); ``` +## Codecs + +Codecs decode a boundary value (wire shape) into a runtime value and encode it +back. `parse`/`safeParse` decode; `encode`/`safeEncode` encode. + +- Built-in: `Ack.date()` (ISO `YYYY-MM-DD` <-> local-midnight `DateTime`), + `Ack.datetime()` (ISO 8601 <-> UTC `DateTime`), `Ack.uri()` (`String` <-> `Uri`), + `Ack.duration()` (milliseconds `int` <-> `Duration`), `Ack.enumCodec(values)` + (enum-name `String` <-> enum value). +- Custom: `Ack.codec(input: ..., decode: ..., encode: ...)`, or + `schema.codec(decode: ..., encode: ...)` on an existing schema. +- `schema.transform(fn)` is one-way (parse only); encoding it fails. +- A codec exports the JSON Schema of its boundary (input) schema. + ## AckType generation `@AckType()` is supported only on: From 4d126fa639efcf34c6e47e6bc69a51e89cb8253e Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 26 Jun 2026 10:31:01 -0400 Subject: [PATCH 3/4] docs: tighten configuration, installation, and guide flow - Configuration: replace duplicated custom-validation and @AckType examples with links. - Installation: add an opener, trim the quickstart-duplicating Basic usage section, fix the generator link. - JSON Schema Integration: lead with the value; group the adapter-author note beside the adapter model. - Common Recipes: add forward links to Codecs and JSON Schema Integration. --- docs/core-concepts/configuration.mdx | 55 ++----------------------- docs/getting-started/installation.mdx | 22 ++-------- docs/guides/common-recipes.mdx | 2 + docs/guides/json-schema-integration.mdx | 9 ++-- 4 files changed, 12 insertions(+), 76 deletions(-) diff --git a/docs/core-concepts/configuration.mdx b/docs/core-concepts/configuration.mdx index 03509647..922f77ab 100644 --- a/docs/core-concepts/configuration.mdx +++ b/docs/core-concepts/configuration.mdx @@ -46,57 +46,8 @@ Ack.integer().min(18); // Default: "Must be at least 18" ### Custom validation logic -Use `.constrain()` for reusable value-level checks, or `.refine()` for cross-field rules. +Use `.constrain()` for reusable value-level checks and `.refine()` for cross-field rules — see [Custom Validation](../guides/custom-validation.mdx). -```dart -import 'package:ack/ack.dart'; - -class IsPositiveConstraint extends Constraint with Validator { - IsPositiveConstraint() - : super( - constraintKey: 'is_positive', - description: 'Number must be positive', - ); - - @override - bool isValid(double value) => value > 0; - - @override - String buildMessage(double value) => 'Number must be positive'; -} - -final priceSchema = Ack.double().constrain(IsPositiveConstraint()); - -// Cross-field validation (e.g., password confirmation) -final signUpSchema = Ack.object({ - 'password': Ack.string().minLength(8), - 'confirmPassword': Ack.string().minLength(8), -}).refine( - (data) => data['password'] == data['confirmPassword'], - message: 'Passwords do not match', -); -``` -*See the [Custom Validation Guide](../guides/custom-validation.mdx) for more patterns.* - -## AckType generation - -`ack_generator` works with top-level schemas annotated with `@AckType()`. Write -the schema, then generate an extension type wrapper around its validated representation: - -```dart -import 'package:ack/ack.dart'; -import 'package:ack_annotations/ack_annotations.dart'; - -part 'user.g.dart'; - -@AckType() -final userSchema = Ack.object({ - 'name': Ack.string().minLength(2).maxLength(50), - 'email': Ack.string().email(), - 'age': Ack.integer().min(18).optional().nullable(), -}, additionalProperties: true); -``` - -After running `dart run build_runner build`, the generated part adds `UserType` with `parse()` / `safeParse()` helpers and typed getters. +## Code generation -*See [TypeSafe Schemas](./typesafe-schemas.mdx) and the generator README for setup details and supported schema shapes.* +Annotate a top-level schema with `@AckType()` to generate a typed wrapper — see [TypeSafe Schemas](./typesafe-schemas.mdx) for setup and supported shapes. diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx index b25f6a0c..6fac59fb 100644 --- a/docs/getting-started/installation.mdx +++ b/docs/getting-started/installation.mdx @@ -2,6 +2,7 @@ title: Installing Ack --- +Ack is a pure-Dart package with no required build step. Add the core library — and, optionally, the code generator for typed wrappers. ## Add to your project @@ -64,26 +65,11 @@ Run the generator: dart run build_runner build --delete-conflicting-outputs ``` -See the [generator documentation](https://docs.page/btwld/ack) for more `@AckType` examples and supported schema shapes. +See [TypeSafe Schemas](../core-concepts/typesafe-schemas.mdx) for more `@AckType` examples and supported schema shapes. -## Basic usage +## Next step -Import Ack in your Dart files: - -```dart -import 'package:ack/ack.dart'; - -final nameSchema = Ack.string().minLength(3); -final result = nameSchema.safeParse('John'); - -if (result.isOk) { - print('Valid: ${result.getOrThrow()}'); -} else { - print('Invalid: ${result.getError()}'); -} -``` - -See the [Quickstart Tutorial](./quickstart-tutorial.mdx) for a complete example. +Import `package:ack/ack.dart` and you're ready — the [Quickstart Tutorial](./quickstart-tutorial.mdx) takes you from your first schema to handling every validation outcome. ## Requirements diff --git a/docs/guides/common-recipes.mdx b/docs/guides/common-recipes.mdx index f4253b68..5e5069ef 100644 --- a/docs/guides/common-recipes.mdx +++ b/docs/guides/common-recipes.mdx @@ -197,5 +197,7 @@ Future fetchUser(String username) async { - [Custom validation guide](./custom-validation.mdx) — deep dive into custom validators - [Flutter form validation](./flutter-form-validation.mdx) — integrate with Flutter forms +- [Codecs](../core-concepts/codecs.mdx) — date, URI, and custom value conversions +- [JSON Schema integration](./json-schema-integration.mdx) — export schemas for API docs and tools - [Schema types](../core-concepts/schemas.mdx) — all available schema types - [Validation rules](../core-concepts/validation.mdx) — complete constraint reference diff --git a/docs/guides/json-schema-integration.mdx b/docs/guides/json-schema-integration.mdx index 83814795..a64df87e 100644 --- a/docs/guides/json-schema-integration.mdx +++ b/docs/guides/json-schema-integration.mdx @@ -2,12 +2,7 @@ title: JSON Schema Integration --- -Ack [schemas](../core-concepts/schemas.mdx) can be converted into JSON Schema objects, letting you generate API documentation directly from your validation schemas. - -> **Building an adapter package?** Most users only need `toJsonSchema()`. If you -> convert Ack schemas to another target format, render from -> `schema.toSchemaModel()` (the canonical `AckSchemaModel`) rather than the JSON -> Schema map — see [Creating Schema Converters](./creating-schema-converter-packages.md). +Your Ack [schema](../core-concepts/schemas.mdx) already describes your data's shape — so you can export it as JSON Schema (Draft-7) and reuse it to document an API, drive a form library, or define an LLM tool, all from one source of truth. ## Generating JSON schemas @@ -38,6 +33,8 @@ void main() { } ``` +> **Building an adapter package?** Most users only need `toJsonSchema()` (above). To convert Ack schemas to another target format, render from `schema.toSchemaModel()` (the canonical `AckSchemaModel`) rather than the JSON Schema map — see [Creating Schema Converters](./creating-schema-converter-packages.md). + ## Adapter model Use `toSchemaModel()` for a reusable, target-independent view of an Ack schema: From 12783eeb3ab52512387897a24e3674ceaf86cb2e Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Fri, 26 Jun 2026 12:48:36 -0400 Subject: [PATCH 4/4] fix(docs): repair docs.page validate check docs.page renders .mdx pages but not .md, so links/sidebar entries to the .md schema-converter guides resolved to missing pages. - Drop the 'For Package Authors' sidebar group (the two guides are .md templates with placeholders that MDX can't render). - Point the JSON Schema Integration adapter note at the GitHub-rendered guide. The guides remain in docs/guides for contributors. Verified with 'npx @docs.page/cli check'. --- docs.json | 13 ------------- docs/guides/json-schema-integration.mdx | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/docs.json b/docs.json index 7ec9d74b..b36272f5 100644 --- a/docs.json +++ b/docs.json @@ -68,19 +68,6 @@ { "title": "Configuration", "href": "/core-concepts/configuration" } ] }, - { - "group": "For Package Authors", - "pages": [ - { - "title": "Schema Converter Quickstart", - "href": "/guides/schema-converter-quickstart" - }, - { - "title": "Creating Schema Converters", - "href": "/guides/creating-schema-converter-packages" - } - ] - }, { "group": "Reference", "pages": [{ "title": "API Reference", "href": "/api-reference/" }] diff --git a/docs/guides/json-schema-integration.mdx b/docs/guides/json-schema-integration.mdx index a64df87e..7a72f07f 100644 --- a/docs/guides/json-schema-integration.mdx +++ b/docs/guides/json-schema-integration.mdx @@ -33,7 +33,7 @@ void main() { } ``` -> **Building an adapter package?** Most users only need `toJsonSchema()` (above). To convert Ack schemas to another target format, render from `schema.toSchemaModel()` (the canonical `AckSchemaModel`) rather than the JSON Schema map — see [Creating Schema Converters](./creating-schema-converter-packages.md). +> **Building an adapter package?** Most users only need `toJsonSchema()` (above). To convert Ack schemas to another target format, render from `schema.toSchemaModel()` (the canonical `AckSchemaModel`) rather than the JSON Schema map — see the [adapter authoring guide on GitHub](https://github.com/btwld/ack/blob/main/docs/guides/creating-schema-converter-packages.md). ## Adapter model