Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@ jobs:
run: |
dart test --platform chrome \
test/schemas/numeric_platform_parity_test.dart \
test/schemas/duration_range_test.dart \
test/schemas/nullability_export_contract_test.dart \
test/schemas/extensions/numeric_extensions_test.dart \
test/schemas/extensions/list_schema_extensions_test.dart \
test/utils/collection_utils_test.dart \
test/json_schema_import_test.dart \
test/json_schema_vocabulary_contract_test.dart \
test/json_schema_import_api_test.dart

- name: Test JSON Schema import bridge in Chrome
Expand Down
5 changes: 3 additions & 2 deletions PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ For example, packages at 1.3.0 can still declare `ack: ^1.2.0` when they use onl
Pub workspaces manage one local dependency resolution, not package versions.
Keep hosted version constraints on sibling dependencies and `resolution:
workspace` in members; do not add local path overrides to publishable manifests.
Keep explicit workspace paths because glob patterns require Dart 3.11 and this
repository supports Dart 3.9. The root and `ack_example` are private and are not
Keep the explicit workspace paths aligned with the package list. The supported
floor is Dart 3.11.0 (Flutter 3.41.2 for the Firebase adapter); preflight checks
those versions separately. The root and `ack_example` are private and are not
part of the five-package release.

References: [Melos versioning](https://melos.invertase.dev/commands/version),
Expand Down
2 changes: 1 addition & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ dart_code_metrics:
prefer-prefixed-global-constants: false
avoid-returning-widgets: false
prefer-trailing-comma: false
# Enum dot-shorthands require Dart 3.10; the workspace SDK floor is 3.9.
# Keep explicit enum qualification as the workspace style.
prefer-shorthands-with-enums: false
# Schema classes extend a non-const AckSchema base, so they cannot declare
# const constructors.
Expand Down
4 changes: 3 additions & 1 deletion docs/getting-started/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,6 @@ Import `package:ack/ack.dart` and you're ready — the [Quickstart Tutorial](./q

## Requirements

- Dart SDK: `>=3.9.0 <4.0.0` for `ack` and `ack_generator`
- Dart SDK: `>=3.11.0 <4.0.0` for all Ack packages.
- Flutter: `>=3.41.2` when using `ack_firebase_ai`.
- Dart 3.12 or later when your models use primary-constructor syntax.
10 changes: 5 additions & 5 deletions docs/getting-started/migration-guide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ Ack is organized into five coordinated packages that share one version number an

| Package | Role | Key Capabilities |
| --- | --- | --- |
| **`ack`** | Core runtime & annotations | • Fluent schema builders: `Ack.string()`, `Ack.integer()`, `Ack.double()`, `Ack.number()`, `Ack.boolean()`, `Ack.list()`, `Ack.map()`, `Ack.object()`, `Ack.any()`, `Ack.literal()`, `Ack.enumString()`, `Ack.enumValues()`, `Ack.anyOf()`, `Ack.discriminated()`, `Ack.lazy()`<br/>• Bidirectional `CodecSchema<Boundary, Runtime>`: `Ack.date()`, `Ack.datetime()`, `Ack.uri()`, `Ack.duration()`, `.codec()`, `.transform()`<br/>• Two-way string normalizers: `.trim()`, `.toLowerCase()`, `.toUpperCase()`<br/>• `JsonMaybe<T>` (`JsonAbsent<T>` / `JsonValue<T>`) for tri-state `PATCH` payloads<br/>• Full JSON Schema Draft 2020-12 runtime (`Ack.fromJsonSchema()`) and exporter (`toJsonSchema()`, `toSchemaModel()`, `toJsonSchemaDraft7()`)<br/>• Sealed `SchemaResult<T>` (`Ok` / `Fail`) and sealed `SchemaError` hierarchy<br/>• Unified codegen annotations in `package:ack/ack.dart`: `@Schemable()`, `@AckKey()`, and `@Validate.*` |
| **`ack`** | Core runtime & annotations | • Fluent schema builders: `Ack.string()`, `Ack.integer()`, `Ack.double()`, `Ack.number()`, `Ack.boolean()`, `Ack.list()`, `Ack.map()`, `Ack.object()`, `Ack.any()`, `Ack.literal()`, `Ack.enumString()`, `Ack.enumValues()`, `Ack.anyOf()`, `Ack.discriminated()`, `Ack.lazy()`<br/>• Bidirectional `CodecSchema<Boundary, Runtime>`: `Ack.date()`, `Ack.datetime()`, `Ack.uri()`, `Ack.duration()`, `.codec()`, `.transform()`<br/>• Two-way string normalizers: `.trim()`, `.toLowerCase()`, `.toUpperCase()`<br/>• `JsonMaybe<T>` (`JsonAbsent<T>` / `JsonValue<T>`) for tri-state `PATCH` payloads<br/>• JSON Schema Draft 2020-12 import and runtime validation (`Ack.fromJsonSchema()`); native schemas export Draft-7, while `toJsonSchema()` preserves imported 2020-12 resources (`toJsonSchemaDraft7()` explicitly lowers them)<br/>• Sealed `SchemaResult<T>` (`Ok` / `Fail`) and sealed `SchemaError` hierarchy<br/>• Unified codegen annotations in `package:ack/ack.dart`: `@Schemable()`, `@AckKey()`, and `@Validate.*` |
| **`ack_generator`** | Two-way code generator | • **Schema-first (`@Schemable()` on a top-level schema)**: Generates an immutable model class, `Model.schema`, `parse`/`safeParse`, `fromJson`/`toJson`/`safeToJson`, typed `copyWith`, and deep `==`/`hashCode` in `<file>.g.dart`<br/>• **Class-first (`@Schemable()` on a `final class` or `sealed class`)**: Supports standard and Dart 3.12+ primary constructors; generates `_<name>Schema`, `<Name>Schema`, and `_$<Name>Ack` in `<file>.g.dart`<br/>• **`.schema.json` codegen (`ack_generator:ack_json_schema`)**: Compiles `lib/**/*.schema.json` assets into `<name>.schema.dart` + `<name>.schema.g.dart` |
| **`ack_mcp_dart`** | MCP tool adapter | • `server.registerAckTool()`, `registerAckInputTool()`, `registerAckOutputTool()`, and `result.decodeAckOutput()` for `mcp_dart` |
| **`ack_firebase_ai`** | Firebase AI (Gemini) adapter | • `schema.toFirebaseAiSchema()` for Gemini structured output and tool declarations |
| **`ack_mcp_dart`** | MCP tool input adapter | • `server.registerAckTool(name, input: schema, callback: ...)`, `schema.toMcpToolInputSchema()`, and `parseMcpToolArguments(...)`; callbacks return `CallToolResult` and own output validation |
| **`ack_firebase_ai`** | Firebase AI (Gemini) adapter | • `schema.toFirebaseAiResponseJsonSchema()` for the map-based `GenerationConfig.responseJsonSchema` field |
| **`ack_json_schema_builder`** | `json_schema_builder` bridge | • Bidirectional `schema.toJsonSchemaBuilder()` and `jsbSchema.toAckSchema()` |

---
Expand All @@ -24,7 +24,7 @@ Ack is organized into five coordinated packages that share one version number an

| Change | Introduced | What Changed | Why It Changed |
| --- | --- | --- | --- |
| **Annotations merged into `package:ack`** | `1.7.0` | `ack_annotations` was retired. `@Schemable()`, `@AckKey()`, and `@Validate.*` ship directly in `package:ack/ack.dart`. | Eliminated version skew between `ack` and a separate annotations package and reduced model files to a single `import 'package:ack/ack.dart';`. |
| **Annotations merged into `package:ack`** | `1.7.0` | `ack_annotations` was retired and model/validation annotations moved into `ack`. In 2.0, the public annotation API is `@Schemable()`, `@AckKey()`, and `@Validate.*` from `package:ack/ack.dart`. | Eliminated version skew between `ack` and a separate annotations package and reduced model files to a single `import 'package:ack/ack.dart';`. |
| **Unified `@Schemable()` annotation** | `1.7.0` | `@Schemable()` drives both schema-first and class-first generation, replacing `@AckInfer()`, `@AckModel()`, and `@AckType()`. | One annotation and one mental model whether you start from an `AckSchema` or a hand-written Dart `class`. |
| **Unified `@Validate.*` validation namespace** | `2.0.0` | All field and parameter validation annotations—including presence (`@Validate.required()`, `@Validate.optional()`), nullability (`@Validate.notNull()`), bounds (`@Validate.min(0)`), formats (`@Validate.email()`, `@Validate.dateTime()`, `@Validate.uri()`), and collection rules (`@Validate.notEmpty()`, `@Validate.uniqueItems()`)—live under `@Validate.*` in `package:ack/ack.dart`. | Top-level 1.x classes (`@Required`, `@Email`, `@Uuid`, `@Date`, `@Uri`, `@DateTime`, `@Pattern`) collided with `package:meta` / Flutter (`Required`), `dart:core` (`Uri`, `DateTime`, `Pattern`), and `package:uuid`, forcing import prefixes (`as ack`). `@Validate.*` has zero symbol collisions and one canonical name per validation rule. |
| **Single `.g.dart` part & direct JSON emitter** | `2.0.0` | `ack_generator:ack_generator` is a `SharedPartBuilder` that emits `<file>.g.dart` directly with no `json_serializable` or `json_annotation` dependency. | In 1.x, every model required two `part` directives (`.ack.dart` + `.ack.g.dart`) and a second `json_serializable` pass. Direct emission cuts build steps in half, uses standard `part '<file>.g.dart';`, and coexists with Freezed or Riverpod in the same file. |
Expand Down Expand Up @@ -93,7 +93,7 @@ All top-level validation and presence classes were removed in `2.0.0`. Replace t
| `Ack.enumCodec(MyEnum.values)` | `2.0.0` | `Ack.enumValues(MyEnum.values)` |
| `StandardSchemaV1` (`standard_schema.dart`) | `2.0.0` | `AckSchema<Boundary, Runtime>` + `SchemaResult<T>` |
| `schema.toMap()` | `2.0.0` | `schema.toJsonSchema()` or `schema.toSchemaModel()` |
| `server.registerAckModelTool(...)` (`ack_mcp_dart`) | `2.0.0` | `server.registerAckTool(name, inputSchema: Model.schema, ...)` |
| `server.registerAckModelTool(...)` (`ack_mcp_dart`) | `2.0.0` | `server.registerAckTool(name, input: Model.schema, callback: ...)`; return a `CallToolResult` from the callback |
| `convertAckSchemaModelToBuilder(model)` (`ack_json_schema_builder`) | `2.0.0` | `schema.toJsonSchemaBuilder()` |
| `Ack.fromJsonSchema(...)` enforcing `format` by default | `2.0.0` | Pass `Ack.fromJsonSchema(schema, assertFormats: true)` (or declare `format-assertion` in `$vocabulary`) when format assertions are required |
| `schema.toJsonSchema()` lowering imported 2020-12 schemas to Draft-7 | `2.0.0` | `schema.toJsonSchema()` now preserves Draft 2020-12 resources by default; call `schema.toJsonSchemaDraft7()` when Draft-7 lowering is required |
Expand Down
78 changes: 48 additions & 30 deletions docs/guides/creating-schema-converter-packages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ final jsonSchema = schema.toJsonSchema();
`AckSchemaModel` describes the boundary shape, constraints, export-safe
defaults, discriminator metadata, and warnings that adapters can reuse for
non-JSON targets. JSON-map adapters can call `schema.toJsonSchema()` directly.
Imported schemas use the existing `AckAllOfSchemaModel` and
`AckRefSchemaModel` variants, with their preserved definition bodies attached
to the root model. They do not introduce an import-only variant into the sealed
model hierarchy.
Imported JSON Schema resources can surface as `AckRawSchemaModel`, directly or
inside composition models, preserving their dialect and resource semantics.
Non-JSON adapters must implement support for those resources or reject them
explicitly. Native lazy schemas surface as `AckRefSchemaModel`; supporting
references also requires resolving their definitions, not just emitting a name.
Adapters should not traverse `AckSchema` subclasses or parse rendered JSON
Schema output as their source of truth for non-JSON formats.

Expand Down Expand Up @@ -141,9 +142,9 @@ repository: https://github.com/btwld/ack
issue_tracker: https://github.com/btwld/ack/issues

environment:
sdk: '>=3.9.0 <4.0.0'
sdk: '>=3.11.0 <4.0.0'
# Add flutter if target SDK requires it
# flutter: '>=3.35.0'
# flutter: '>=3.41.2'

dependencies:
ack: ^2.0.0-alpha.1
Expand Down Expand Up @@ -343,6 +344,9 @@ class <Target>SchemaConverter {
AckRefSchemaModel() => throw UnsupportedError(
'Reference schemas (Ack.lazy) are not supported by this target.',
),
AckRawSchemaModel() => throw UnsupportedError(
'Imported JSON Schema resources are not supported by this target.',
),
};
}

Expand All @@ -351,7 +355,7 @@ class <Target>SchemaConverter {
// ========================================================================

static <TargetSchema> _convertString(AckStringSchemaModel schema) {
final enumValues = schema.allowedStringValues;
final enumValues = schema.enumValues;
if (enumValues != null) {
return _buildEnumSchema(enumValues, schema);
}
Expand Down Expand Up @@ -399,7 +403,8 @@ class <Target>SchemaConverter {

static <TargetSchema> _convertObject(AckObjectSchemaModel schema) {
final properties = <String, TargetSchema>{};
for (final entry in schema.properties?.entries ?? const []) {
for (final entry in
schema.properties?.entries ?? const <MapEntry<String, AckSchemaModel>>[]) {
properties[entry.key] = _convertModel(entry.value);
}

Expand Down Expand Up @@ -1193,6 +1198,9 @@ TargetSchema _convert(AckSchemaModel schema) {
AckAllOfSchemaModel() => _convertAllOf(schema),
AckNullSchemaModel() => TargetSchema.nullValue(),
AckRefSchemaModel() => TargetSchema.ref(schema.refName),
AckRawSchemaModel() => throw UnsupportedError(
'Implement target-specific support for imported JSON Schema resources.',
),
};
}

Expand Down Expand Up @@ -1246,23 +1254,24 @@ TargetSchema _convert(AckSchema schema) {

### Handling Transformed Schemas

`toSchemaModel()` projects transformed schemas from their boundary input schema
and preserves adapter-safe metadata such as description, nullability, and
`x-transformed`.
`toSchemaModel()` projects codecs and transforms from their boundary input
schema. It describes external data, not the decoded Dart runtime value.
Decoder, encoder, and refinement callbacks still belong to Ack runtime
validation; they cannot be reproduced by a target schema renderer.

**Option 1: Render projected boundary model** (Recommended)
**Render the projected boundary model**:
```dart
TargetSchema _convertTransformed(AckSchemaModel schema) {
return _convert(schema);
}
```

**Option 2: Reject explicit transformed marker** (If target cannot tolerate transforms)
```dart
if (schema.extensions['x-transformed'] == true) {
throw UnsupportedError('Transformed Ack schemas are not supported here.');
}
```
Native transforms do not emit an `x-transformed` detection marker. An imported
document may preserve that extension as ordinary annotation data, but it is not
a reliable way to identify native callbacks. Validate returned data with the
original Ack schema, and inspect export warnings for omitted runtime checks.
Use a bidirectional `.codec()` when the application also needs runtime-to-wire
encoding; `.transform()` is parse-only.

### Handling Discriminated Unions

Expand Down Expand Up @@ -1375,6 +1384,12 @@ class OpenApiSchemaConverter {

**Target**: GraphQL Schema Definition Language

This illustration supports object roots with scalar fields and arrays of those
scalars. Field names and `typeName` must already be valid GraphQL identifiers.
Nested objects, enums, compositions, references, and imported raw resources are
rejected until their target definitions are implemented. It is not a lossless
translation of Ack constraints; for example, GraphQL `Int` has its own range.

```dart
// lib/src/converter.dart
import 'package:ack/ack.dart';
Expand All @@ -1396,15 +1411,20 @@ class GraphQlSchemaConverter {
if (schema is! AckObjectSchemaModel) {
throw UnsupportedError('Only object schema models can be converted.');
}
if (schema.properties?.isNotEmpty != true) {
throw UnsupportedError('GraphQL object definitions need at least one field.');
}

buffer.writeln('type $typeName {');

final required = schema.required ?? const <String>[];
for (final entry in schema.properties?.entries ?? const []) {
for (final entry in
schema.properties?.entries ?? const <MapEntry<String, AckSchemaModel>>[]) {
final fieldName = entry.key;
final fieldSchema = entry.value;
final gqlType = _getGraphQLType(fieldSchema);
final nullableSuffix = required.contains(fieldName) ? '!' : '';
final nullableSuffix =
required.contains(fieldName) && !fieldSchema.nullable ? '!' : '';

if (fieldSchema.description != null) {
buffer.writeln(' """${fieldSchema.description}"""');
Expand All @@ -1417,22 +1437,20 @@ class GraphQlSchemaConverter {

static String _getGraphQLType(AckSchemaModel schema) {
return switch (schema) {
AckStringSchemaModel(allowedStringValues: final values)
when values != null && values.isNotEmpty =>
_generateEnumType(schema),
AckStringSchemaModel(enumValues: final values)
when values != null =>
throw UnsupportedError('Generate enum definitions before using enums.'),
AckStringSchemaModel() => 'String',
AckIntegerSchemaModel() => 'Int',
AckNumberSchemaModel() => 'Float',
AckBooleanSchemaModel() => 'Boolean',
AckArraySchemaModel(items: final item?) => '[${_getGraphQLType(item)}]',
_ => 'String', // Fallback
AckArraySchemaModel(items: final item?) =>
'[${_getGraphQLType(item)}${item.nullable ? '' : '!'}]',
_ => throw UnsupportedError(
'Only scalar fields and arrays of supported scalars are implemented.',
),
};
}

static String _generateEnumType(AckStringSchemaModel schema) {
// Would need to generate enum definitions separately
return 'EnumType';
}
}
```

Expand Down
Loading
Loading