diff --git a/components/FloatingNavbar.tsx b/components/FloatingNavbar.tsx index baa9b8bc..2106608c 100644 --- a/components/FloatingNavbar.tsx +++ b/components/FloatingNavbar.tsx @@ -296,7 +296,7 @@ const DOCS_SECTIONS: Partial> = { label: 'Advanced', pages: [ { label: 'Codecs', href: '/documentation/ack/advanced/codecs' }, - { label: 'TypeSafe Schemas', href: '/documentation/ack/advanced/typesafe-schemas' }, + { label: 'Model Code Generation', href: '/documentation/ack/advanced/typesafe-schemas' }, { label: 'JSON Schema Integration', href: '/documentation/ack/advanced/json-schema-integration' }, { label: 'Configuration', href: '/documentation/ack/advanced/configuration' }, { label: 'Adapter Package Quickstart', href: '/documentation/ack/advanced/schema-converter-quickstart' }, diff --git a/components/ProductFooter.tsx b/components/ProductFooter.tsx index 9a977d42..fa432faf 100644 --- a/components/ProductFooter.tsx +++ b/components/ProductFooter.tsx @@ -71,7 +71,7 @@ export default function ProductFooter() {
Built for trustworthy Dart boundaries.
- GitHub + GitHub pub.dev llms.txt
diff --git a/components/constants.ts b/components/constants.ts index 16ac845e..722b1133 100644 --- a/components/constants.ts +++ b/components/constants.ts @@ -20,5 +20,5 @@ export const CONCEPTA_STRUCTURED_ADDRESS = { export const MIX_GITHUB_URL = 'https://github.com/btwld/mix' export const REMIX_GITHUB_URL = 'https://github.com/btwld/remix' export const NAKED_UI_GITHUB_URL = 'https://github.com/btwld/naked_ui' -export const ACK_GITHUB_URL = 'https://github.com/btwld/ack' +export const ACK_GITHUB_URL = 'https://github.com/conceptadev/ack' export const ROCKETS_GITHUB_URL = 'https://github.com/btwld/rockets' diff --git a/components/landing/ack/AckHome.tsx b/components/landing/ack/AckHome.tsx index f58c837c..91c305f3 100644 --- a/components/landing/ack/AckHome.tsx +++ b/components/landing/ack/AckHome.tsx @@ -31,7 +31,7 @@ export function AckHome() { Start validating - + View on GitHub
@@ -190,14 +190,14 @@ export function AckHome() {
- TYPE-SAFE CODE GENERATION -

Generate types, not duplicate models.

-

Annotate the schema you already trust. Ack generates a lightweight wrapper with typed getters and parse helpers—without changing the runtime schema.

- Generate typed schemas + TWO-WAY MODEL GENERATION +

Own the schema—or the class.

+

Use @AckInfer() to generate an immutable model from a schema, or @AckModel() to derive a validated schema and JSON helpers from your class.

+ Generate models and schemas
user_schema.dart · optional generator -
@AckType(){`\n`}final userSchema = Ack.object(...);{`\n\n`}final ada = UserType.parse(json);{`\n`}print(ada.email); // String
+
@AckInfer(){`\n`}final userSchema ={`\n`}  Ack.object(...);{`\n`}// → User{`\n\n`}@AckModel(){`\n`}final class Account{`\n`}    with _$AccountAck {'{'} ... {'}'}{`\n`}// → AccountSchema
diff --git a/public/ack/llms.txt b/public/ack/llms.txt index 7c4e756c..0ea010bc 100644 --- a/public/ack/llms.txt +++ b/public/ack/llms.txt @@ -1,16 +1,17 @@ # Ack -> A schema validation library for Dart and Flutter with a fluent runtime API and `@AckType()`-driven extension-type generation. Version 1.1.0. +> A schema validation library for Dart and Flutter with a fluent runtime API, bidirectional codecs, and two-way model/schema code generation. -Ack validates external data with hand-written schemas built using the `Ack` -factory. When you want typed wrappers over validated values, annotate top-level -schema variables or getters with `@AckType()` and run `ack_generator`. +Ack validates untrusted boundary data with schemas built through the `Ack` +factory. Annotate a schema with `@AckInfer()` to generate a model, or annotate a +hand-written class with `@AckModel()` to generate its schema and JSON helpers. +There is no `@AckSchema()` annotation; `AckSchema` is the runtime schema type. ## Packages -1. `ack`: core runtime validation library -2. `ack_annotations`: exposes `@AckType()` -3. `ack_generator`: generates extension types for annotated top-level schemas +1. `ack`: core runtime validation, codecs, and generated-model support +2. `ack_annotations`: exposes `@AckInfer()` and `@AckModel()` +3. `ack_generator`: generates models from schemas and schemas from models 4. `ack_firebase_ai`: converts Ack schemas to Firebase AI structured-output schemas 5. `ack_json_schema_builder`: converts Ack schemas to `json_schema_builder` schemas @@ -33,169 +34,177 @@ 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. +Codecs decode a boundary value 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`; rejects leap-second strings - because Dart cannot represent them), `Ack.uri()` (`String` <-> `Uri`), - `Ack.duration()` (milliseconds `int` <-> `Duration`), `Ack.enumCodec(values)` - (enum-name `String` <-> enum value). +- Built in: `Ack.date()`, `Ack.datetime()`, `Ack.uri()`, `Ack.duration()`, and + `Ack.enumCodec(values)`. - 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. +- `schema.transform(fn)` is one-way. It works for runtime parsing, but it is + rejected by generated models because they must encode back to the boundary. +- A codec exports the JSON Schema of its boundary schema. -## AckType generation +## Model code generation -`@AckType()` is supported only on: +Both directions use the same generated parts and build command. `@AckInfer()` +is schema-first; `@AckModel()` is class-first. -- top-level schema variables -- top-level schema getters - -It is not supported on classes or instance members. - -Example: +Every annotated library declares both generated parts: ```dart import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; -part 'user_schema.g.dart'; +part 'user_schema.ack.dart'; +part 'user_schema.ack.g.dart'; -@AckType() +@AckInfer() final addressSchema = Ack.object({ 'street': Ack.string(), 'city': Ack.string(), }); -@AckType() +@AckInfer() final userSchema = Ack.object({ 'name': Ack.string(), 'address': addressSchema, }); ``` -Generated capabilities: - -- `UserType.parse(data)` -- `UserType.safeParse(data)` -- typed getters such as `String get name` and `AddressType get address` +Run `dart run build_runner build`. `addressSchema` and `userSchema` generate +`Address` and `User`. A custom `@AckInfer(name: 'Member')` value is used exactly. -## Supported AckType schema shapes +Generated object models provide: -- `Ack.object(...)` -- `Ack.string()` -- `Ack.integer()` -- `Ack.double()` -- `Ack.boolean()` -- `Ack.list(...)` -- `Ack.literal(...)` -- `Ack.enumString(...)` -- `Ack.enumValues(...)` -- explicit transforms such as `.transform(...)` -- `Ack.discriminated(...)` with the constraints below +- an unchecked constructor with stored typed fields; +- `User.parse(data)` and `User.safeParse(data)` for schema validation; +- `User.fromJson(json)`, `toJson()`, and `safeToJson()` for the JSON boundary; +- generated `copyWith` (null means keep the current value), deep + collection-aware `==`/`hashCode`, and `toString`; +- a public static `User.$ack` adapter for nested generated models; +- an unmodifiable `additionalProperties` map for passthrough objects. -Not supported for extension-type generation: +For class-first generation: -- `Ack.any()` -- `Ack.anyOf()` -- transformed object schemas -- transformed discriminated schemas - -## Discriminated AckType schemas +```dart +@AckModel(caseStyle: AckCaseStyle.snake) +final class Account with _$AccountAck { + const Account({required this.displayName, this.role = 'member'}); -`Ack.discriminated(...)` works with `@AckType()` when: + @MinLength(2) + final String displayName; + final String role; -- the base schema is a top-level `@AckType()` declaration -- `schemas` is a non-empty map literal -- each branch is a top-level schema variable/getter reference -- 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 + static final fromJson = AccountSchema.fromJson; +} +``` -Example: +This generates an `AccountSchema` facade backed by a private `_accountObject` +wire schema and `_accountSchema` codec. Instantiable models apply the +`_$AccountAck` mixin for `toJson`, `safeToJson`, `copyWith`, and deep +equality. The facade exposes `schema`, `wireSchema`, `parse`, `safeParse`, +`fromJson`, `encode`, `safeEncode`, `toJsonSchema`, and `toSchemaModel`. +`schemaName:` overrides the exact public UpperCamelCase facade name; no public +lower-camel alias is emitted. Constructor parameters determine presence and +defaults. Field types and constraint annotations determine the schema. Sealed +classes use `@AckModel(discriminatorKey: ...)`; same-library concrete branches +are included automatically. Use `@AckField` to override `schema` and/or +`AckFieldPresence`. Unknown properties use `AckAdditionalPropertiesMode` +(`reject` by default; `discard` or `capture`). + +Nested class-first models compose through `AddressSchema.schema`, preserving +import prefixes. Import combinators and barrels must expose both `Address` and +`AddressSchema`. Schema-first declarations may explicitly use the facade, and +class-first fields may use schema-first generated model types; both directions +work from a clean build. Nullable/list/set wrappers are preserved. Automatic +recursive class-first graphs are rejected; use schema-first named `Ack.lazy` +for recursive contracts. + +Generated models do not implement `Map`, `List`, or scalar interfaces. Scalar +and collection roots are value models with a `.value` field. Use model fields +and `toJson()` instead of treating a model as its old boundary representation. + +## Supported schema-first generated-model shapes + +- objects and empty objects; +- string, integer, double, number, boolean, list, literal, and enum roots; +- built-in and custom bidirectional codecs; +- lists, sets, and string-keyed `Map` runtime values; +- named nested models, aliases, defaults, and additional properties; +- direct, prefixed, and re-exported model and runtime type references; +- named `Ack.lazy` self-recursion and mutual recursion; +- same-library discriminated unions. + +Generation rejects nullable roots, one-way transforms, `Ack.any()`, +`Ack.anyOf()`, bare `Ack.instance()`, anonymous inline object fields, +non-string map keys, unresolved dynamic factories, name collisions, and +cross-library discriminated branches. + +Class-first generation supports object models, inferred scalar and enum +fields, nested lists, sets through list codecs, custom `@AckField` schemas, +constructor defaults, case styles, `AckAdditionalPropertiesMode`, and +same-library sealed discriminated unions. It rejects `dynamic`, `Object?`, +non-String map keys, recursive class-first graphs, class-first value roots, +undiscriminated `anyOf` models, missing `_$ClassAck` mixins, and no-op +`@AckField()`. + +## Discriminated generated models + +`Ack.discriminated(...)` works with `@AckInfer()` when: + +- `schemas` is a non-empty map literal; +- each branch is a top-level `@AckInfer()` object schema in the same library; +- each branch is non-nullable; +- branch schemas normally omit the discriminator field; +- an included discriminator is an exact matching `Ack.literal(...)` or an + `Ack.enumString(...)` containing the branch key. ```dart -@AckType() -final catSchema = Ack.object({ - 'lives': Ack.integer(), -}); +@AckInfer() +final catSchema = Ack.object({'lives': Ack.integer()}); -@AckType() -final dogSchema = Ack.object({ - 'breed': Ack.string(), -}); +@AckInfer() +final dogSchema = Ack.object({'breed': Ack.string()}); -@AckType() +@AckInfer() final petSchema = Ack.discriminated( discriminatorKey: 'type', - schemas: { - 'cat': catSchema, - 'dog': dogSchema, - }, + schemas: {'cat': catSchema, 'dog': dogSchema}, ); ``` -`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: - -```dart -@AckType() -final catSchema = Ack.object({ - 'type': Ack.literal('cat'), // allowed, but usually unnecessary - 'lives': Ack.integer(), -}); -``` - -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. - -## Resolution rules - -AckType generation is intentionally strict: - -- nested object fields must reference named top-level schemas -- inline anonymous object schemas are rejected for typed generation -- cross-file direct imports, prefixed imports, and re-exports are supported -- unannotated object schema references fail generation instead of silently falling back to raw maps -- circular schema alias/reference chains fail generation +This generates a sealed `Pet` base and final `Cat` and `Dog` branches. +Boundary payloads include the discriminator. Generated subtype parsing +validates through the union's effective branch. + +## Migration from the previous generator + +- Rename `@AckType()` to `@AckInfer()` for each connected graph you opt in. +- Add both `.ack.dart` and `.ack.g.dart` part directives. +- Rename generated `UserType` usages to `User` unless a custom name is set. +- Replace map/list/scalar interface access with stored fields or `.value`. +- Replace passthrough `.args` access with `.additionalProperties`. +- Replace legacy Map access and `.args` with typed fields and + `.additionalProperties`; use `fromJson` / `toJson` at the JSON boundary. +- Replace one-way transforms used by generated models with bidirectional codecs. +- Replace public class-first `accountSchema` calls with `AccountSchema`; use + `AccountSchema.schema` when composing another schema and + `AccountSchema.wireSchema` for the raw Map schema. +- Apply the generated `_$ClassAck` mixin; do not keep extension-based helpers. +- Replace `additionalProperties: bool` with `AckAdditionalPropertiesMode`. +- Regenerate all checked-in outputs with `dart run build_runner build`. ## Runtime API reminders -- `schema.parse(data)` throws on invalid input -- `schema.safeParse(data)` returns `SchemaResult` -- `safeParse` turns invalid input and recoverable `Exception` values from - constraint/refinement callbacks into contextual failures -- `Error` values from constraint/refinement callbacks are rethrown with their - original stack trace -- codec/transform decoders and `safeParseAs` mappers turn thrown values, - including `Error` values, into `SchemaTransformError` failures -- `SchemaResult.getOrThrow()` returns the validated value or throws `AckException` -- `.optional()` allows a field to be omitted -- `.nullable()` allows a present field to hold `null` -- object schemas support `additionalProperties: true` -- `schema.toSchemaModel()` returns `AckSchemaModel`, the canonical boundary/export model for adapters -- `schema.toJsonSchema()` renders `schema.toSchemaModel().toJsonSchema()` -- adapter packages should render from `AckSchemaModel`, not by traversing `AckSchema` subclasses -- `Ack.list(...)` does not support nullable item schemas; make the list itself nullable when needed -- `Ack.lazy(...)` defaults `maxDepth` to `100` for recursive parsing, runtime - validation, and encoding; the runtime-only limit is omitted from exported schemas with a warning -- `Ack.object`, `Ack.anyOf`, and enum factories snapshot their input collections; - unions and enum value lists must be non-empty, and enum values must be unique -- lengths and item counts must be non-negative; numeric bounds must be finite; - `multipleOf` must be finite and greater than zero - -## Build command - -```bash -dart run build_runner build -``` +- `schema.parse(data)` throws on invalid input. +- `schema.safeParse(data)` returns `SchemaResult`. +- `SchemaResult.getOrThrow()` returns the value or throws `AckException`. +- `.optional()` allows an object field to be omitted. +- `.nullable()` allows a present value to be null. +- `schema.toSchemaModel()` returns the canonical adapter/export model. +- `schema.toJsonSchema()` renders that model as JSON Schema. +- `Ack.list(...)` does not support nullable item schemas. +- `Ack.lazy(...)` defaults `maxDepth` to 100. +- Ack snapshots schema factory collections, and collection bounds must be valid. +- Generated data classes use `deepEquals` and `deepHashCode` from `package:ack/ack.dart`. diff --git a/site-tests/ack-integration.test.mjs b/site-tests/ack-integration.test.mjs index 2fb49583..bddea9c8 100644 --- a/site-tests/ack-integration.test.mjs +++ b/site-tests/ack-integration.test.mjs @@ -106,7 +106,7 @@ test('resolves every Ack documentation and landing-page link', () => { assert.deepEqual(failures, []) }) -test('features codecs and code generation on Ack entry pages', () => { +test('features codecs and two-way model generation on Ack entry pages', () => { const landing = readFileSync(ackLanding, 'utf8') const overview = readFileSync( join(ackContentRoot, 'getting-started/overview.mdx'), @@ -115,12 +115,44 @@ test('features codecs and code generation on Ack entry pages', () => { for (const source of [landing, overview]) { assert.match(source, /Codecs|CODECS/) - assert.match(source, /Code generation|CODE GENERATION/) + assert.match(source, /Model code generation|MODEL GENERATION/) + assert.match(source, /@AckInfer/) + assert.match(source, /@AckModel/) + assert.doesNotMatch(source, /@AckType|UserType/) assert.match(source, /\/documentation\/ack\/advanced\/codecs/) assert.match(source, /\/documentation\/ack\/advanced\/typesafe-schemas/) } }) +test('keeps Ack 1.2 setup and generated-part guidance current', () => { + const installation = readFileSync( + join(ackContentRoot, 'getting-started/installation.mdx'), + 'utf8', + ) + const generation = readFileSync( + join(ackContentRoot, 'advanced/typesafe-schemas.mdx'), + 'utf8', + ) + const llms = readFileSync(join(root, 'public/ack/llms.txt'), 'utf8') + const ackSurface = [ + readFileSync(ackLanding, 'utf8'), + ...walk(ackContentRoot).map((path) => readFileSync(path, 'utf8')), + ].join('\n') + + assert.match(installation, /ack: \^1\.2\.0/) + assert.match(installation, /ack_annotations: \^1\.2\.0/) + assert.match(installation, /ack_generator: \^1\.2\.0/) + for (const source of [installation, generation, llms]) { + assert.match(source, /part '[-_a-z]+\.ack\.dart';/) + assert.match(source, /part '[-_a-z]+\.ack\.g\.dart';/) + assert.match(source, /@AckInfer/) + assert.match(source, /@AckModel/) + } + assert.doesNotMatch(ackSurface, /github\.com\/btwld\/ack/) + assert.doesNotMatch(ackSurface, /@AckType|UserType/) + assert.match(llms, /Rename `@AckType\(\)` to `@AckInfer\(\)`/) +}) + test('showcases a User codec round trip across the Ack landing page', () => { const landing = readFileSync(ackLanding, 'utf8') diff --git a/src/content/documentation/ack/advanced/_meta.js b/src/content/documentation/ack/advanced/_meta.js index ff587624..8569cce1 100644 --- a/src/content/documentation/ack/advanced/_meta.js +++ b/src/content/documentation/ack/advanced/_meta.js @@ -1,7 +1,7 @@ const meta = { codecs: 'Codecs', - 'typesafe-schemas': 'TypeSafe Schemas', + 'typesafe-schemas': 'Model Code Generation', 'json-schema-integration': 'JSON Schema Integration', configuration: 'Configuration', 'adapter-authors': { diff --git a/src/content/documentation/ack/advanced/configuration.mdx b/src/content/documentation/ack/advanced/configuration.mdx index a360483c..8002d82d 100644 --- a/src/content/documentation/ack/advanced/configuration.mdx +++ b/src/content/documentation/ack/advanced/configuration.mdx @@ -94,7 +94,9 @@ Use `.constrain()` for reusable value-level checks and `.refine()` for cross-fie ## Code generation -Annotate a top-level schema with `@AckType()` to generate a typed wrapper — see [TypeSafe Schemas](/documentation/ack/advanced/typesafe-schemas) for setup and supported shapes. +Annotate a top-level schema with `@AckInfer()` to generate an immutable model, +or annotate a hand-written class with `@AckModel()` to generate its schema. +See [Model Code Generation](/documentation/ack/advanced/typesafe-schemas) for both workflows. ## Related guides diff --git a/src/content/documentation/ack/advanced/creating-schema-converter-packages.mdx b/src/content/documentation/ack/advanced/creating-schema-converter-packages.mdx index 8e281a1b..eb9e8220 100644 --- a/src/content/documentation/ack/advanced/creating-schema-converter-packages.mdx +++ b/src/content/documentation/ack/advanced/creating-schema-converter-packages.mdx @@ -47,7 +47,7 @@ Schema output as their source of truth for non-JSON formats. ### Package Naming Convention -```text +``` ack_ ``` @@ -65,7 +65,7 @@ ack_ ### Directory Layout -```text +``` packages/ack_/ ├── lib/ │ ├── ack_.dart # Main library file (public API) @@ -135,16 +135,16 @@ touch .pubignore name: ack_ description: schema converter for Ack validation library version: 1.0.0-beta.1 -repository: https://github.com/btwld/ack -issue_tracker: https://github.com/btwld/ack/issues +repository: https://github.com/conceptadev/ack +issue_tracker: https://github.com/conceptadev/ack/issues environment: - sdk: '>=3.8.0 <4.0.0' + sdk: '>=3.9.0 <4.0.0' # Add flutter if target SDK requires it # flutter: '>=3.16.0' dependencies: - ack: ^1.0.0 + ack: ^1.2.0 # Add target SDK dependency if needed # : ^x.y.z meta: ^1.15.0 @@ -177,7 +177,7 @@ analyzer: #### 1.4 Configure .pubignore -```text +``` # Development and IDE files .claude/ .idea/ @@ -1023,14 +1023,14 @@ Converts Ack schemas to format for [use case]. Assumes familiarity with \`\`\`yaml dependencies: - ack: ^1.0.0 + ack: ^1.2.0 ack_: ^1.0.0 : ^x.y.z # Required peer dependency \`\`\` ### Compatibility -Requires `: >=x.y.z : >=x.y.z -v1.0.0-beta.1 +[1.0.0-beta.1]: https://github.com/conceptadev/ack/releases/tag/ack_-v1.0.0-beta.1 ``` --- @@ -1515,6 +1515,6 @@ class GraphQlSchemaConverter { - Document limitations as you discover them **For help**: -- Create GitHub issue: https://github.com/btwld/ack/issues +- Create GitHub issue: https://github.com/conceptadev/ack/issues - Reference this guide - Ask specific questions about target system diff --git a/src/content/documentation/ack/advanced/schema-converter-quickstart.mdx b/src/content/documentation/ack/advanced/schema-converter-quickstart.mdx index 05c83e95..4887af0f 100644 --- a/src/content/documentation/ack/advanced/schema-converter-quickstart.mdx +++ b/src/content/documentation/ack/advanced/schema-converter-quickstart.mdx @@ -37,14 +37,14 @@ touch .pubignore name: ack_ description: schema converter for Ack validation library version: 1.0.0-beta.1 -repository: https://github.com/btwld/ack +repository: https://github.com/conceptadev/ack environment: - sdk: '>=3.8.0 <4.0.0' + sdk: '>=3.9.0 <4.0.0' # flutter: '>=3.16.0' # Uncomment if needed dependencies: - ack: ^1.0.0 + ack: ^1.2.0 # : ^x.y.z # Add if needed meta: ^1.15.0 @@ -328,7 +328,7 @@ void main() { \`\`\`yaml dependencies: - ack: ^1.0.0 + ack: ^1.2.0 ack_: ^1.0.0 \`\`\` @@ -351,7 +351,7 @@ final targetSchema = schema.toSchema(); ## License -Part of the [Ack](https://github.com/btwld/ack) monorepo. +Part of the [Ack](https://github.com/conceptadev/ack) monorepo. ``` ## 9. Verify Setup (2 minutes) @@ -391,4 +391,4 @@ dart format . ## Reference -See [Creating Adapter Packages](/documentation/ack/advanced/creating-schema-converter-packages) for detailed guidance. +See [Creating Schema Converter Packages](/documentation/ack/advanced/creating-schema-converter-packages) for detailed guidance. diff --git a/src/content/documentation/ack/advanced/typesafe-schemas.mdx b/src/content/documentation/ack/advanced/typesafe-schemas.mdx index fed4d6d2..d4a5c146 100644 --- a/src/content/documentation/ack/advanced/typesafe-schemas.mdx +++ b/src/content/documentation/ack/advanced/typesafe-schemas.mdx @@ -1,127 +1,404 @@ --- -title: TypeSafe Schemas -description: Generate typed wrappers for Ack schemas using the @AckType annotation and code generator +title: Model Code Generation +description: Generate immutable models from Ack schemas or derive Ack schemas from Dart classes --- -# TypeSafe Schemas +# Model Code Generation -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. +Ack supports code generation in both directions. You can start with a schema +and generate a Dart model, or start with a Dart model and generate its schema. -## Overview +| Starting point | Annotation | Generated result | +| --- | --- | --- | +| An Ack schema | `@AckInfer()` | An immutable Dart model | +| A Dart class | `@AckModel()` | An Ack codec schema and JSON 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`. -4. Use the generated `TypeName.parse()` / `TypeName.safeParse()` helpers. +There is no `@AckSchema()` annotation. `AckSchema` is the +runtime type returned by factories such as `Ack.string()` and `Ack.object()`. +The two code-generation annotations are `@AckInfer()` and `@AckModel()`. -## Basic usage +## Install the generator + +```bash +dart pub add ack ack_annotations +dart pub add --dev ack_generator build_runner +``` + +Every annotated library needs both generated parts: + +```dart +part 'models.ack.dart'; +part 'models.ack.g.dart'; +``` + +Run the generator after adding or changing a model: + +```bash +dart run build_runner build +``` + +## A working example of both directions + +This file contains one schema-first model and one class-first model. Both use +the same builders and generated parts. ```dart import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; -part 'user_schema.g.dart'; +part 'models.ack.dart'; +part 'models.ack.g.dart'; -@AckType() -final addressSchema = Ack.object({ - 'street': Ack.string(), - 'city': Ack.string(), +// Schema-first: write the schema; Ack generates Order. +@AckInfer() +final orderSchema = Ack.object({ + 'id': Ack.string(), + 'total': Ack.double().positive(), }); -@AckType() -final userSchema = Ack.object({ - 'id': Ack.string(), - 'email': Ack.string().email().nullable(), - 'address': addressSchema, +// Class-first: write Account; Ack generates AccountSchema. +@AckModel(caseStyle: AckCaseStyle.snake) +final class Account with _$AccountAck { + const Account({ + required this.displayName, + required this.email, + required this.middleName, + this.website, + this.role = 'member', + }); + + @MinLength(2) + final String displayName; + + @Email() + final String email; + + final String? middleName; + final Uri? website; + final String role; + + static final fromJson = AccountSchema.fromJson; +} +``` + +After generation, both directions have a typed parsing and JSON boundary: + +```dart +final order = Order.parse({'id': 'o1', 'total': 12.5}); +print(order.total); // double + +final account = Account.fromJson({ + 'display_name': 'Ada', + 'email': 'ada@example.com', + 'middle_name': null, }); +print(account.role); // member +print(account.toJson()); // validated snake_case JSON ``` -The generated part file contains `AddressType` and `UserType` extension types, each with typed field getters and `parse()` / `safeParse()` static methods. +The schema-first and class-first declarations may live in the same library. +Ordinary `@JsonSerializable()` classes keep their separate `.g.dart` file, +but do not put `@AckModel()` and `@JsonSerializable()` on the same class. + +## Schema-first with `@AckInfer()` -The type name drops a trailing `Schema` and adds `Type` (`userSchema` → `UserType`). Override it with `@AckType(name: 'AppUser')`, which generates `AppUserType`. +Use schema-first generation when the wire contract is the primary artifact. +The source schema owns validation, defaults, codecs, and JSON Schema export; +Ack generates the stored Dart type around it. -## Supported schema shapes +```dart +@AckInfer() +final userSchema = Ack.object({ + 'id': Ack.string().uuid(), + 'email': Ack.string().email().nullable(), + 'tags': Ack.list(Ack.string()).optional(), +}); +``` -`@AckType()` supports: +`userSchema` generates `User`: a trailing `Schema` is removed and no suffix is +added. Use `@AckInfer(name: 'AppUser')` when you need an exact class name. -- `Ack.object(...)` -- `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 +Generated models provide: -`Ack.any()` and `Ack.anyOf()` are not supported. +- an unchecked constructor with stored typed fields; +- `User.parse` and `User.safeParse` for validated input; +- `User.fromJson`, `toJson`, and `safeToJson` for the JSON boundary; +- generated `copyWith`, deep collection-aware `==`/`hashCode`, and `toString`; +- a public `User.$ack` adapter used by nested generated models. -## Discriminated schemas +The generator supports object and value roots, literals, enums, lists, sets, +string-keyed maps, built-in and custom bidirectional codecs, named nested +models, aliases, defaults, additional properties, named lazy recursion, and +same-library discriminated unions. Stored collections are copied recursively +into unmodifiable collections. -`Ack.discriminated(...)` works with `@AckType()` when all of the following hold: +One-way transforms cannot back a generated model because there is no encoder. +Generation also rejects nullable roots, `Ack.any()`, `Ack.anyOf()`, bare +`Ack.instance()`, anonymous inline object fields, unresolved dynamic schema +factories, and cross-library union branches. -- `schemas` is a non-empty map literal -- 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 +### Schema-first unions -Example: +Each branch must be a named `@AckInfer()` object schema in the same library: ```dart -@AckType() -final catSchema = Ack.object({ - 'lives': Ack.integer(), -}); +@AckInfer() +final catSchema = Ack.object({'lives': Ack.integer()}); -@AckType() -final dogSchema = Ack.object({ - 'breed': Ack.string(), -}); +@AckInfer() +final dogSchema = Ack.object({'breed': Ack.string()}); -@AckType() +@AckInfer() final petSchema = Ack.discriminated( discriminatorKey: 'type', - schemas: { - 'cat': catSchema, - 'dog': dogSchema, - }, + schemas: {'cat': catSchema, 'dog': dogSchema}, ); ``` -`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: +This generates a sealed `Pet` base plus `Cat` and `Dog` branches. + +## Class-first with `@AckModel()` + +Use class-first generation when the Dart class is the primary artifact. Ack +reads constructor-backed fields and generates a codec schema whose runtime +value is your class: + +```dart +final profile = ProfileSchema.parse(payload); // Profile +final result = ProfileSchema.safeParse(payload); +final json = profile.toJson(); +``` + +Ack generates a public `ProfileSchema` facade and keeps the codec itself in a +library-private `_profileSchema` variable. The facade is the only public schema +entry point: + +- `ProfileSchema.parse` and `safeParse` validate input; +- `ProfileSchema.fromJson` is the one-argument map convenience; +- `ProfileSchema.encode` and `safeEncode` validate while encoding; +- `ProfileSchema.toJsonSchema()` exports the boundary JSON Schema; +- `ProfileSchema.toSchemaModel()` exports Ack's canonical schema model; +- `ProfileSchema.schema` is the typed model codec used for composition; +- `ProfileSchema.wireSchema` is the raw structural `Map` schema. + +Every instantiable `@AckModel` class and implicit sealed-union branch must +apply its generated `_$ClassAck` mixin. The mixin supplies `toJson()`, +`safeToJson()`, a typed `copyWith` that treats `null` as "keep the current +value", and deep collection-aware `==`, `hashCode`, and `toString`. A sealed +abstract base receives union serialization only. + +Generated `toJson()` and `safeToJson()` methods delegate through that same +facade. Ack cannot inject a constructor into a hand-written class, so the +recommended conventional entry point is an inferred static tear-off: + +```dart +static final fromJson = ProfileSchema.fromJson; +``` + +This is a callable static field. If a framework specifically requires a +constructor, use: + +```dart +factory Profile.fromJson(Map json) => + ProfileSchema.fromJson(json); +``` + +An explicit function-field type is also valid but normally unnecessary: ```dart -@AckType() -final catSchema = Ack.object({ - 'type': Ack.literal('cat'), // allowed, but usually unnecessary - 'lives': Ack.integer(), +static final Profile Function(Map) fromJson = + ProfileSchema.fromJson; +``` + +Use `@AckModel(schemaName: 'WireProfileSchema')` to override the exact public +facade name. It must be a public UpperCamel identifier. The private backing +name remains derived from the model class, and no public lower-camel alias is +generated. + +### Presence comes from the constructor + +| Declaration | Input behavior | Encoding behavior | +| --- | --- | --- | +| `required T` | Required, non-null | Always present | +| `required T?` | Required, may be null | Present even when null | +| Optional `T?` | May be omitted | Omitted when null | +| Constructor default | Missing input uses the default | Encodes the stored value, including null | + +Nullable defaults keep their source-level behavior. With +`this.label = 'fallback'`, missing input and JSON `null` both parse as +`'fallback'`, while a directly constructed `label: null` still encodes as JSON +null. With `this.label = null`, missing input and JSON `null` parse as null and +the encoded object keeps the key with a null value. + +The generator infers `String`, `bool`, numeric types, `DateTime`, `Uri`, +`Duration`, enums, nested lists, and sets. Sets use a list codec. + +Constraint annotations follow the field type: + +- numeric: `@Min`, `@Max`, `@MultipleOf`, `@Positive`, `@Negative`; +- strings: `@MinLength`, `@MaxLength`, `@Pattern`, `@Email`, `@NotEmpty`; +- lists and sets: `@MinItems`, `@MaxItems`, `@UniqueItems`. + +Use `caseStyle` for model-wide JSON names. Supported values are `none`, +`snake`, `kebab`, `pascal`, and `screamingSnake`. Use the re-exported +`@JsonKey(name: 'wire_name')` for a single field override. Ack resolves each +wire key once and uses it for both validation and JSON mapping. + +### Custom field schemas + +Use `@AckField` when inference is not enough. `schema` is a const tear-off of a +top-level function returning an `AckSchema`. `presence` overrides constructor +inference. At least one of those arguments is required: + +```dart +final class Color { + const Color(this.hex); + final String hex; +} + +AckSchema colorSchema() => Ack.string().codec( + decode: Color.new, + encode: (color) => color.hex, +); + +@AckModel() +final class Theme with _$ThemeAck { + const Theme({required this.primary}); + + @AckField(schema: colorSchema) + final Color primary; +} +``` + +`Map` fields also use `@AckField`; class-first generation does not +invent an `Ack.map()` runtime API. Non-String map keys, `dynamic`, and +`Object?` fields are rejected because they do not provide a static schema. + +### Class-first unions + +Annotate a sealed base with a discriminator key. Concrete branches in the same +library are included automatically, and inherited constructor fields may use +super parameters: + +```dart +@AckModel(discriminatorKey: 'type') +sealed class Pet with _$PetAck { + const Pet({required this.id}); + final String id; +} + +@AckModel(discriminatorValue: 'cat') +final class Cat extends Pet with _$CatAck { + const Cat({required super.id, required this.lives}); + final int lives; +} + +final class Dog extends Pet with _$DogAck { + const Dog({required super.id, required this.breed}); + final String breed; +} +``` + +The base and every concrete branch receive facades (`PetSchema`, `CatSchema`, +and `DogSchema`), including branches without their own `@AckModel` annotation. + +Without an explicit `discriminatorValue`, the wire value is the verbatim class +name (`Dog` above). Set explicit values when the wire format must remain stable +through class renames. Class-first `anyOf` and value roots are not supported; +use schema-first generation for those shapes. + +### Additional properties + +`@AckModel` uses `AckAdditionalPropertiesMode`: + +- `reject` (default) fails validation on unknown properties; +- `discard` accepts unknown properties but does not store them; +- `capture` stores them in `additionalPropertiesField`, which defaults to + `additionalProperties` and may be `args`. + +Capture requires a declared `Map` field initialized by the +constructor. Encoding writes extras first, so a declared field or +discriminator cannot be replaced by an extra value. + +`@AckField` can override inferred presence with `AckFieldPresence.required` or +`optional`. A no-op `@AckField()` is rejected. `optional` is allowed only when +the constructor can accept a missing value, with a discriminator exception for +union branches. + +### Reusing generated schemas + +Nested class-first fields compose through the target facade automatically: + +```dart +import 'address.dart' as address; + +@AckModel() +final class Order with _$OrderAck { + const Order({required this.shipping}); + final address.Address shipping; +} + +// Generated field schema: address.AddressSchema.schema +``` + +Schema-first declarations can use the same facade explicitly: + +```dart +@AckInfer() +final envelopeSchema = Ack.object({ + 'address': address.AddressSchema.schema, + 'history': Ack.list(address.AddressSchema.schema), }); ``` -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. +Class-first fields may also use a model generated by `@AckInfer()`; Ack composes +through the generated `Address.$ack.schema`. Both directions work on the first +clean build, including nullable values, lists, nested lists, and sets. + +An import combinator must expose both the hand-written model and its facade: + +```dart +import 'address.dart' show Address, AddressSchema; +``` -## Resolution rules +The same rule applies to barrel exports. Prefixed imports avoid ambiguity when +two libraries declare the same model name. Deferred imports are unsupported. -- 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 rather than silently falling back to raw maps. -- Circular alias/reference chains fail generation with a clear error. +Automatic recursive class-first schemas are not yet defined. For self or +mutually recursive contracts, use schema-first named `Ack.lazy` schemas. -## Limitations +## Which direction should you choose? -- `@AckType()` only works on top-level schema variables and getters. -- Nullable top-level schemas do not emit extension types. -- `Ack.list(...)` rejects nullable item schemas. Make the list itself nullable - with `Ack.list(item).nullable()` when the whole field may be null. -- Use `.transform(...)` with an explicit output type so the generator can infer the representation type. +Choose `@AckInfer()` when you want to design the boundary schema first, generate +the whole model, or model a scalar or collection root. Choose `@AckModel()` +when you already own the class, want to keep methods and constructors in source, +or prefer field annotations over a separate object schema. -## Build checklist +This choice is per model, not per project. A migration can keep existing +schema-first types while new domain-owned types use class-first generation. -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`. +## Limit generation in larger projects -## Next steps +Use matching `generate_for` entries for both Ack phases. This reduces analyzer +work and keeps the JSON phase scoped to libraries whose `.ack.dart` input is +generated: + +```yaml +targets: + $default: + builders: + ack_generator|ack_models: + generate_for: [lib/models/**.dart] + ack_generator|ack_model_json: + generate_for: [lib/models/**.dart] +``` + +Both entries must cover the same annotated libraries. + +## Build checklist -- [JSON Serialization](/documentation/ack/essentials/json-serialization) — parse JSON straight into generated types -- [Common Recipes](/documentation/ack/how-to-guides/common-recipes) — patterns that combine schemas and generated types -- [API Reference](/documentation/ack/reference/api-reference) — core API quick reference and generated API docs +1. Add `ack` and `ack_annotations` to dependencies. +2. Add `ack_generator` and `build_runner` to dev dependencies. +3. Declare both generated parts in each annotated library. +4. Choose `@AckInfer()` for schema-first or `@AckModel()` for class-first. +5. Run `dart run build_runner build`. diff --git a/src/content/documentation/ack/essentials/json-serialization.mdx b/src/content/documentation/ack/essentials/json-serialization.mdx index a8ad0950..0bcbb3e8 100644 --- a/src/content/documentation/ack/essentials/json-serialization.mdx +++ b/src/content/documentation/ack/essentials/json-serialization.mdx @@ -1,6 +1,6 @@ --- title: JSON Serialization -description: Validate, encode, and parse JSON data with Ack schemas and typed wrappers +description: Validate, encode, and parse JSON data with Ack schemas and generated models --- # JSON Serialization @@ -48,7 +48,7 @@ void processApiResponse(String jsonString) { print('Valid JSON received: $validDataMap'); // Pass the validated map to your own model layer, - // or use an AckType-generated wrapper (see next section). + // or use an Ack-generated model (see next section). } else { // Handle validation errors (see the Error Handling guide). print('Invalid JSON data: ${result.getError()}'); @@ -66,7 +66,7 @@ processApiResponse('not valid json'); // Decoding error ## Working with validated data -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: +After successful validation, `result.getOrThrow()` returns a `Map` whose structure and types match your schema. You can work with it directly, pass it into your own model class, or parse it into a generated immutable model: ```dart final result = userSchema.safeParse(jsonData); @@ -80,25 +80,25 @@ if (result.isOk) { // Option 2: Pass the validated map into your own model layer // - Constructor: User(name: validData['name'], age: validData['age']) - // - json_serializable: User.fromJson(Map.from(validData)) + // - generated model: User.parse(validData) // - freezed: User.fromJson(Map.from(validData)) // - dart_mappable: UserMapper.fromMap(validData) // - Manual factory: User.fromMap(validData) } ``` -## Parsing JSON into typed wrappers +## Parsing JSON into generated models -Once a schema is annotated with `@AckType()` (see [TypeSafe Schemas](/documentation/ack/advanced/typesafe-schemas) for setup), parse JSON straight into typed getters — no manual casting: +Once a schema is annotated with `@AckInfer()` (see [Model Code Generation](/documentation/ack/advanced/typesafe-schemas) for setup), parse JSON into a generated immutable model. Public `fromJson` / `toJson` still cross the source Ack schema; `json_serializable` only maps the already-validated runtime value onto stored fields. ```dart import 'dart:convert'; final jsonData = jsonDecode('{"name": "Alice", "email": "alice@example.com"}'); -final result = UserType.safeParse(jsonData); +final result = User.safeParse(jsonData); if (result.isOk) { - final user = result.getOrThrow()!; + final user = result.getOrThrow(); print(user.name); // typed String print(user.email); // typed String? } @@ -152,4 +152,4 @@ conversions. validates values and encodes codec runtime values back to their boundary representation. - **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. +- **Model conversion:** After validation, how you convert the validated map into an app model is up to you. Generated models still parse and encode through an Ack schema; `json_serializable` only maps that runtime value onto stored fields. `@AckInfer()` starts from the schema, while `@AckModel()` starts from your class. diff --git a/src/content/documentation/ack/getting-started/installation.mdx b/src/content/documentation/ack/getting-started/installation.mdx index c5030810..49c96529 100644 --- a/src/content/documentation/ack/getting-started/installation.mdx +++ b/src/content/documentation/ack/getting-started/installation.mdx @@ -1,11 +1,11 @@ --- title: Installing Ack -description: Install the Ack validation library and its optional code generator +description: Install the Ack validation library and its optional two-way model code generator --- # 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. +Ack is a pure-Dart package with no required build step. Add the core library — and, optionally, the code generator for immutable models. ## Add to your project @@ -23,12 +23,13 @@ Or add to your `pubspec.yaml` (check [pub.dev](https://pub.dev/packages/ack) for ```yaml dependencies: - ack: ^1.0.0 # Replace with latest version + ack: ^1.2.0 # Replace with latest version ``` -## Code generator (`@AckType()`) +## Optional code generator -To generate typed wrappers for hand-written schemas, add the annotation and generator packages alongside `ack`: +To generate models from Ack schemas or Ack schemas from hand-written models, +add the annotation and generator packages alongside `ack`: ```bash dart pub add ack ack_annotations @@ -39,27 +40,40 @@ Or add them to your `pubspec.yaml`: ```yaml dependencies: - ack: ^1.0.0 # Replace with latest version - ack_annotations: ^1.0.0 # Replace with latest version + ack: ^1.2.0 # Replace with latest version + ack_annotations: ^1.2.0 # Replace with latest version dev_dependencies: - ack_generator: ^1.0.0 # Replace with latest version + ack_generator: ^1.2.0 # Replace with latest version 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. +The generator works in both directions. `@AckInfer()` marks a top-level schema +and generates an immutable model. `@AckModel()` marks a hand-written class and +generates a public `Schema` facade, a private Ack codec, and JSON +helpers. There is no `@AckSchema()` annotation; `AckSchema` is the runtime +schema type. ```dart import 'package:ack/ack.dart'; import 'package:ack_annotations/ack_annotations.dart'; -part 'user.g.dart'; +part 'user.ack.dart'; +part 'user.ack.g.dart'; -@AckType() +@AckInfer() final userSchema = Ack.object({ 'name': Ack.string(), 'email': Ack.string().email(), }); + +@AckModel() +final class Account with _$AccountAck { + const Account({required this.name}); + final String name; + + static final fromJson = AccountSchema.fromJson; +} ``` Run the generator: @@ -68,7 +82,8 @@ Run the generator: dart run build_runner build ``` -See [TypeSafe Schemas](/documentation/ack/advanced/typesafe-schemas) for more `@AckType` examples and supported schema shapes. +See [Model Code Generation](/documentation/ack/advanced/typesafe-schemas) for working +tutorials, supported shapes, and guidance on choosing a direction. ## Next step @@ -76,4 +91,5 @@ Import `package:ack/ack.dart` and you're ready — the [Quickstart Tutorial](/do ## Requirements -- Dart SDK: `>=3.8.0 <4.0.0` +- Dart SDK: `>=3.9.0 <4.0.0` for core `ack` +- Dart SDK: `>=3.9.0 <4.0.0` when using `ack_annotations` / `ack_generator` diff --git a/src/content/documentation/ack/getting-started/overview.mdx b/src/content/documentation/ack/getting-started/overview.mdx index 44b0c722..f5744400 100644 --- a/src/content/documentation/ack/getting-started/overview.mdx +++ b/src/content/documentation/ack/getting-started/overview.mdx @@ -31,7 +31,7 @@ if (result.isOk) { } ``` -On success, `getOrThrow()` returns your **validated data** as a `Map`. Ack checks and shapes the data — opt into [code generation](/documentation/ack/advanced/typesafe-schemas) when you'd rather have typed getters than map access. +On success, `getOrThrow()` returns your **validated data** as a `Map`. Ack checks and shapes the data — opt into [model code generation](/documentation/ack/advanced/typesafe-schemas) when you'd rather have typed fields than map access or want a schema generated from an existing class. ## Why Ack? @@ -60,13 +60,13 @@ On success, `getOrThrow()` returns your **validated data** as a `Map - Code generation -

Keep the schema. Add a typed API.

+ Model code generation +

Start from the contract you own.

- Add @AckType() to the schema you already trust and generate lightweight wrappers with typed getters, parse helpers, and no duplicated model. + Use @AckInfer() to generate an immutable model from a schema, or @AckModel() to derive a validated schema and JSON helpers from your class.
- UserType.parse(json) - Generate typed schemas → + @AckInfer() → User · @AckModel() → UserSchema + Explore model generation → @@ -77,7 +77,7 @@ On success, `getOrThrow()` returns your **validated data** as a `Map()`, and anonymous inline objects -For `Ack.discriminated(...)` constraints with `@AckType`, see -[Type-safe Schemas](/documentation/ack/advanced/typesafe-schemas#discriminated-schemas). +For `Ack.discriminated(...)` constraints with `@AckInfer`, see +[Model Code Generation](/documentation/ack/advanced/typesafe-schemas#schema-first-unions). **Example:** ```dart -@AckType() +import 'package:ack/ack.dart'; +import 'package:ack_annotations/ack_annotations.dart'; + +part 'user.ack.dart'; +part 'user.ack.g.dart'; + +@AckInfer() final userSchema = Ack.object({ 'name': Ack.string(), 'email': Ack.string().email(), }); // Generated: -// - extension type UserType(Map _data) { ... } +// - final class User { ... } // - The schema variable remains unchanged // Usage: -final user = UserType.parse({'name': 'Alice', 'email': 'alice@example.com'}); +final user = User.parse({'name': 'Alice', 'email': 'alice@example.com'}); print(user.name); // Type-safe String access print(user.email); // Type-safe String access +print(user.toJson()); ``` +### `@AckModel()` + +**Target**: Public, constructable classes + +**Generates**: A public `Schema` facade backed by a private raw +`wireSchema` and typed codec, plus the `_$ClassAck` mixin for `toJson`, +`safeToJson`, `copyWith`, and deep collection-aware `==` / `hashCode` / +`toString` + +Ack infers schema fields from constructor-backed fields. Required parameters, +nullable types, optional parameters, and constructor defaults determine field +presence. Use `caseStyle` for model-wide JSON naming and `@JsonKey(name: ...)` +for a field override. Instantiable models and implicit union branches must +apply the generated mixin. `copyWith` treats `null` as "keep the current +value." + +**Inferred field types:** strings, booleans, numeric types, `DateTime`, `Uri`, +`Duration`, enums, nested lists, and sets + +**Constraint annotations:** + +- numeric: `@Min`, `@Max`, `@MultipleOf`, `@Positive`, `@Negative` +- strings: `@MinLength`, `@MaxLength`, `@Pattern`, `@Email`, `@NotEmpty` +- collections: `@MinItems`, `@MaxItems`, `@UniqueItems` + +Use `@AckField` to override `schema` and/or `AckFieldPresence`. A no-op +`@AckField()` is rejected. A sealed base annotated with +`@AckModel(discriminatorKey: ...)` generates a discriminated union from its +same-library concrete branches. Unknown properties use +`AckAdditionalPropertiesMode` (`reject` by default; `discard` or `capture`). + +```dart +@AckModel(caseStyle: AckCaseStyle.snake) +final class Account with _$AccountAck { + const Account({required this.displayName, this.role = 'member'}); + + @MinLength(2) + final String displayName; + final String role; + + static final fromJson = AccountSchema.fromJson; +} + +final account = AccountSchema.parse({'display_name': 'Ada'}); +print(account.toJson()); +print(AccountSchema.toJsonSchema()); +``` + +The facade exposes `schema`, `wireSchema`, `parse`, `safeParse`, `fromJson`, +`encode`, `safeEncode`, `toJsonSchema`, and `toSchemaModel`. The backing +`_accountSchema` is library-private and no public lower-camel alias is +generated. Set `schemaName: 'WireAccountSchema'` to choose the exact facade +class name. + +Nested class-first fields compose through `AddressSchema.schema`. Imports with +combinators must expose both names (`show Address, AddressSchema`). A +schema-first `@AckInfer()` declaration may also compose the facade explicitly, +and class-first models can use schema-first generated model types on a clean +build. + +`@AckModel()` cannot share a class with `@JsonSerializable()`. Class-first +value roots, automatic recursive graphs, and undiscriminated `anyOf` shapes are +unsupported; use schema-first `@AckInfer()` and named `Ack.lazy` for those +cases. See +[Model Code Generation](/documentation/ack/advanced/typesafe-schemas) for the complete +tutorial and comparison. + ### `EnumSchema` Schema for mapping enum `.name` strings at the boundary to typed enum values at diff --git a/src/content/documentation/ack/reference/llms-txt.mdx b/src/content/documentation/ack/reference/llms-txt.mdx index c007c6a2..1151f981 100644 --- a/src/content/documentation/ack/reference/llms-txt.mdx +++ b/src/content/documentation/ack/reference/llms-txt.mdx @@ -5,6 +5,9 @@ description: Find Ack's machine-readable llms.txt index for AI agents and tools # AI & llms.txt -Ack publishes a compact, machine-readable documentation index at [`/ack/llms.txt`](/ack/llms.txt). It is generated alongside the library documentation and links AI agents to the canonical guides and API surface. +Ack publishes a focused, machine-readable product summary at [`/ack/llms.txt`](/ack/llms.txt). It mirrors the repository root [`llms.txt`](https://github.com/conceptadev/ack/blob/main/llms.txt) so tools can discover Ack's packages, runtime API, code-generation workflows, and migration notes without parsing HTML. -The repository root [`llms.txt`](https://github.com/btwld/ack/blob/main/llms.txt) remains the canonical source. The website serves the same content as plain text so tools can discover it without parsing HTML. +The canonical docs.page site also generates a page index and a full documentation export automatically: + +- [`docs.page/conceptadev/ack/llms.txt`](https://docs.page/conceptadev/ack/llms.txt) lists every documentation page with its title, summary, and canonical URL. +- [`docs.page/conceptadev/ack/llms-full.txt`](https://docs.page/conceptadev/ack/llms-full.txt) bundles the full MDX source for one-shot ingestion.