diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..5c6d472c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,41 @@ +# Code of Conduct + +## Our commitment + +Ack's community is committed to a welcoming, respectful, and harassment-free +experience for everyone, regardless of background, identity, experience level, +or personal characteristics. + +## Expected behavior + +- Be kind, constructive, and specific when giving feedback. +- Assume good intent while taking responsibility for your impact. +- Respect differing viewpoints, boundaries, and levels of experience. +- Keep technical disagreements focused on the work. +- Protect private or sensitive information shared during a report. + +## Unacceptable behavior + +- Harassment, discrimination, threats, or personal attacks. +- Sexualized language, imagery, or unwelcome attention. +- Trolling, sustained disruption, or deliberately inflammatory comments. +- Publishing another person's private information without permission. +- Any conduct that would reasonably be considered inappropriate in a + professional community. + +## Reporting and enforcement + +If you experience or witness unacceptable behavior, report it privately to the +repository owner through the contact methods on their +[GitHub profile](https://github.com/btwld). Do not open a public issue for a +sensitive conduct report. If no private contact method is available, open an +issue that asks the owner to establish private contact, without including the +report details. + +Maintainers will review reports fairly, protect the reporter's privacy as far as +possible, and may remove content, issue a warning, temporarily restrict +participation, or permanently ban a participant. Retaliation against anyone who +makes a good-faith report is not tolerated. + +This policy applies in project spaces and when someone is publicly representing +the Ack community. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7ba23d89..7ae7eba3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,8 @@ # Contributing to Ack Thanks for helping improve Ack. Keep changes focused, tested, and easy to -review. +review. By participating, you agree to follow the +[Code of Conduct](./CODE_OF_CONDUCT.md). ## Development setup @@ -54,3 +55,9 @@ Use `!` or a `BREAKING CHANGE:` footer for breaking API changes. User-facing changes should update the relevant package `CHANGELOG.md`. Release publishing is handled by maintainers through `PUBLISHING.md`. + +## Getting help and reporting security issues + +Use [SUPPORT.md](./SUPPORT.md) for questions and public bug reports. Follow +[SECURITY.md](./SECURITY.md) for vulnerabilities; do not disclose suspected +security issues in a public issue. diff --git a/README.md b/README.md index 65ef8dfe..594990ed 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,13 @@ This repository is a monorepo containing: - **[ack_json_schema_builder](./packages/ack_json_schema_builder)**: Converter to `json_schema_builder` schemas - **[example](./example)**: Example projects demonstrating usage of all packages +## Community and support + +- Read [CONTRIBUTING.md](./CONTRIBUTING.md) before proposing a change. +- Use [SUPPORT.md](./SUPPORT.md) for questions, bug reports, and feature requests. +- Report vulnerabilities privately as described in [SECURITY.md](./SECURITY.md). +- Participation is governed by the [Code of Conduct](./CODE_OF_CONDUCT.md). + ## Quick start ### Core library (ack) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..94ae2d3b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,34 @@ +# Security Policy + +## Supported versions + +Security fixes are made for the latest released Ack version. Users should +upgrade to the newest compatible release before reporting a problem or asking +for a backport. Older release lines may receive fixes at the maintainers' +discretion. + +## Reporting a vulnerability + +Do not report a suspected vulnerability in a public GitHub issue. + +Contact the repository owner privately through the contact methods on their +[GitHub profile](https://github.com/btwld). Include: + +- the affected package and version; +- a clear description of the impact; +- steps or a minimal program that reproduces the issue; +- any known workarounds; and +- whether the report or exploit details have been shared elsewhere. + +If no private contact method is available, open an issue that requests a +private security contact without including vulnerability details. + +Please allow maintainers time to confirm the report and coordinate a fix before +public disclosure. If the report is accepted, the project will credit the +reporter unless they prefer to remain anonymous. + +## Scope + +Security reports should describe a concrete confidentiality, integrity, or +availability impact. General bugs, unexpected validation results, and feature +requests belong in the public [issue tracker](https://github.com/btwld/ack/issues). diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 00000000..4862c501 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,30 @@ +# Support + +## Start with the documentation + +- [Ack documentation](https://docs.page/btwld/ack) +- [Quickstart tutorial](https://docs.page/btwld/ack/getting-started/quickstart-tutorial) +- [API quick reference](https://docs.page/btwld/ack/api-reference/) +- [Generated API documentation](https://pub.dev/documentation/ack/latest/ack/) + +Search existing [GitHub issues](https://github.com/btwld/ack/issues) before +opening a new one; your question or bug may already have an answer. + +## Ask a question + +Open a [GitHub issue](https://github.com/btwld/ack/issues/new) and apply the +`question` label. Include the Ack package and version, your Dart or Flutter +version, what you are trying to accomplish, and a small reproducible example. + +## Report a bug or request a feature + +Use the [issue tracker](https://github.com/btwld/ack/issues) with the `bug` or +`enhancement` label. A minimal reproduction and the full error output make an +issue much easier to investigate. + +For vulnerabilities, follow [SECURITY.md](./SECURITY.md) and do not open a +public issue. Conduct concerns should follow +[CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md). + +Ack is maintained by volunteers. Response times vary, and support is provided +on a best-effort basis. diff --git a/docs.json b/docs.json index 5755aea2..eba30e5a 100644 --- a/docs.json +++ b/docs.json @@ -68,6 +68,19 @@ { "title": "Configuration", "href": "/core-concepts/configuration" } ] }, + { + "group": "Adapter Authors", + "pages": [ + { + "title": "Adapter Package Quickstart", + "href": "/guides/schema-converter-quickstart" + }, + { + "title": "Creating Adapter Packages", + "href": "/guides/creating-schema-converter-packages" + } + ] + }, { "group": "Reference", "pages": [{ "title": "API Reference", "href": "/api-reference/" }] diff --git a/docs/api-reference/index.mdx b/docs/api-reference/index.mdx index f20401d4..0d988510 100644 --- a/docs/api-reference/index.mdx +++ b/docs/api-reference/index.mdx @@ -2,7 +2,10 @@ title: API Reference --- -Quick reference for the core Ack classes, methods, and annotations. For detailed explanations and examples, see the specific guides linked below. +This page is a curated quick reference for the core Ack classes, methods, and +annotations. Use the [generated API documentation](https://pub.dev/documentation/ack/latest/ack/) +for every public declaration and exact signatures; use the linked guides here +for explanations and examples. ## Core `Ack` Class @@ -83,6 +86,8 @@ Primitive schemas (`StringSchema`, `IntegerSchema`, `DoubleSchema`, `NumberSchem ### Utility Methods - `Map toJsonSchema()`: Returns a Draft-7 JSON Schema map via the canonical `AckSchemaModel` boundary. +- `AckSchemaModel toSchemaModel()`: Returns the canonical, target-independent + boundary model for schema adapters, including export warnings. - `Map toMap()`: Serializes the schema for debugging. See also [Schema Types](../core-concepts/schemas.mdx) for detailed usage examples. diff --git a/docs/core-concepts/configuration.mdx b/docs/core-concepts/configuration.mdx index 922f77ab..d44467d2 100644 --- a/docs/core-concepts/configuration.mdx +++ b/docs/core-concepts/configuration.mdx @@ -8,18 +8,24 @@ Ack has no global configuration object. All behavior is configured at the schema Define behavior when you create your schemas. -### Required fields +### Optional and nullable values -Fields in [`Ack.object`](./schemas.mdx#object) are required by default. Mark a field as optional with `.optional()`. +Fields in [`Ack.object`](./schemas.mdx#object) are required and non-nullable by +default. Use `.optional()` when a field may be omitted, `.nullable()` when a +present field may be `null`, or both when either state is valid. ```dart Ack.object({ 'id': Ack.integer(), - 'name': Ack.string(), - 'email': Ack.string().email().optional() // Email is optional (can be omitted) -}); // id and name are required by default + 'nickname': Ack.string().optional(), + 'middleName': Ack.string().nullable(), + 'avatarUrl': Ack.string().url().optional().nullable(), +}); ``` +See [Optional vs nullable](./schemas.mdx#optional-vs-nullable) for the complete +presence and nullability matrix. + ### Additional properties Control how extra fields are handled with the `additionalProperties` parameter in [`Ack.object`](./schemas.mdx#object). @@ -34,14 +40,49 @@ Ack.object({'id': Ack.integer()}, additionalProperties: false); Ack.object({'id': Ack.integer()}, additionalProperties: true); ``` +Use strict objects for data you own and want to detect drift in. At external +API boundaries, `additionalProperties: true` can make a schema resilient to +new response fields while still validating every field your application uses. + +### Default values + +Use `.withDefault(value)` when a `null` or missing object field should receive +a runtime value during parsing. The default must satisfy the schema's runtime +type and constraints. + +```dart +final settingsSchema = Ack.object({ + 'theme': Ack.enumString(['light', 'dark']).withDefault('light'), + 'pageSize': Ack.integer().min(1).max(100).withDefault(20), +}); +``` + +Defaults change the parsed result; `.optional()` only changes whether an object +field is required. + +### Schema metadata + +Attach a description with `.describe()` when the schema is also used for JSON +Schema export or adapter packages. + +```dart +final userIdSchema = Ack.string() + .uuid() + .describe('Stable identifier for a user account'); +``` + ### Custom error messages -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). +Built-in constraints provide default messages. APIs that accept `message:`, +such as `.matches()`, can replace that message inline. Use `.constrain()` to +provide a message for a reusable custom constraint. See [Custom error +messages](./error-handling.mdx#custom-error-messages). ```dart -Ack.string().minLength(5); // Default: "Too short, min 5 characters" -Ack.integer().min(18); // Default: "Must be at least 18" +final usernameSchema = Ack.string().matches( + r'^[a-z0-9_]+$', + message: 'Use lowercase letters, numbers, and underscores only.', +); ``` ### Custom validation logic @@ -51,3 +92,10 @@ 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](./typesafe-schemas.mdx) for setup and supported shapes. + +## Related guides + +- [Schema Types](./schemas.mdx) — factories, composition, and transformations +- [Validation Rules](./validation.mdx) — built-in constraints +- [Codecs](./codecs.mdx) — bidirectional boundary/runtime configuration +- [JSON Schema Integration](../guides/json-schema-integration.mdx) — exported metadata and defaults diff --git a/docs/core-concepts/json-serialization.mdx b/docs/core-concepts/json-serialization.mdx index f10e4c7b..9405e7bf 100644 --- a/docs/core-concepts/json-serialization.mdx +++ b/docs/core-concepts/json-serialization.mdx @@ -2,7 +2,10 @@ title: JSON Serialization --- -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. +Ack validates decoded JSON at your application boundary. On the way back out, +plain schemas produce JSON-native Dart values directly, while +[codecs](./codecs.mdx) encode rich runtime values such as `DateTime` and `Uri` +back to their wire representation. ## Validating JSON data @@ -24,8 +27,8 @@ final userSchema = Ack.object({ void processApiResponse(String jsonString) { // 1. Decode JSON string into a Dart object. - // Note: jsonDecode returns dynamic, so the structure is unknown initially. - dynamic jsonData; + // jsonDecode returns an unknown structure until the schema validates it. + Object? jsonData; try { jsonData = jsonDecode(jsonString); } catch (e) { @@ -98,8 +101,52 @@ if (result.isOk) { } ``` +## Encoding validated data + +For schemas whose runtime values are already JSON-native (`String`, `num`, +`bool`, `List`, `Map`, and `null`), pass the validated value to `jsonEncode`: + +```dart +final result = userSchema.safeParse({ + 'name': 'Alice', + 'age': 30, + 'email': 'alice@example.com', +}); + +if (result.isOk) { + final validData = result.getOrThrow()!; + final jsonString = jsonEncode(validData); + print(jsonString); +} +``` + +When a schema decodes boundary values into richer Dart types, encode through +the schema before calling `jsonEncode`. This applies each codec in the nested +structure and restores the JSON-safe boundary shape: + +```dart +final eventSchema = Ack.object({ + 'name': Ack.string(), + 'startsAt': Ack.datetime(), +}); + +final event = eventSchema.parse({ + 'name': 'Launch', + 'startsAt': '2026-01-15T14:00:00Z', +}); + +final boundaryData = eventSchema.encode(event); +final jsonString = jsonEncode(boundaryData); +``` + +Use `safeEncode()` when you want a `SchemaResult` instead of an exception. +See [Codecs](./codecs.mdx) for runtime invariants and custom bidirectional +conversions. + ## Key considerations -- **`dart:convert`:** Use `jsonEncode`/`jsonDecode` for JSON string conversion. Ack handles validation only. +- **`dart:convert`:** Use `jsonDecode` and `jsonEncode` for JSON text. Ack + 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. diff --git a/docs/core-concepts/schemas.mdx b/docs/core-concepts/schemas.mdx index 04523bb5..76ca8df7 100644 --- a/docs/core-concepts/schemas.mdx +++ b/docs/core-concepts/schemas.mdx @@ -41,7 +41,7 @@ final nameSchema = Ack.string(); final usernameSchema = Ack.string() .minLength(3) .maxLength(20) - .matches(r'[a-zA-Z0-9_]+'); + .matches(r'^[a-zA-Z0-9_]+$'); // Email validation final emailSchema = Ack.string().email(); @@ -136,7 +136,7 @@ final userSchema = Ack.object({ 'address': Ack.object({ 'street': Ack.string(), 'city': Ack.string(), - 'zipCode': Ack.string().matches(r'\d{5}'), + 'zipCode': Ack.string().matches(r'^\d{5}$'), }), }); ``` @@ -439,7 +439,7 @@ final userWithAgeSchema = Ack.object({ // Type transformation final dateSchema = Ack.string() - .matches(r'\d{4}-\d{2}-\d{2}') + .matches(r'^\d{4}-\d{2}-\d{2}$') .transform((s) => DateTime.parse(s)); ``` diff --git a/docs/core-concepts/typesafe-schemas.mdx b/docs/core-concepts/typesafe-schemas.mdx index 59b61cff..915e469e 100644 --- a/docs/core-concepts/typesafe-schemas.mdx +++ b/docs/core-concepts/typesafe-schemas.mdx @@ -121,4 +121,4 @@ Conflicting discriminator fields, broad `Ack.string()`, and transformed or refin - [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 +- [API Reference](../api-reference/index.mdx) — core API quick reference and generated API docs diff --git a/docs/core-concepts/validation.mdx b/docs/core-concepts/validation.mdx index dba5e873..bab5c8f8 100644 --- a/docs/core-concepts/validation.mdx +++ b/docs/core-concepts/validation.mdx @@ -369,7 +369,7 @@ You can chain multiple constraints. Ack evaluates them in the order you apply th final usernameSchema = Ack.string() .minLength(3) // First: check min length .maxLength(20) // Second: check max length - .matches(r'[a-z0-9_]+') // Third: check pattern (lowercase alphanumeric/underscore) + .matches(r'^[a-z0-9_]+$') // Third: require only lowercase letters, digits, or underscores .notEmpty(); // Redundant if minLength(>0) is used, but illustrates chaining final quantitySchema = Ack.integer() diff --git a/docs/getting-started/quickstart-tutorial.mdx b/docs/getting-started/quickstart-tutorial.mdx index 44a38f75..93c4a3f5 100644 --- a/docs/getting-started/quickstart-tutorial.mdx +++ b/docs/getting-started/quickstart-tutorial.mdx @@ -26,11 +26,9 @@ See [Schema Types](../core-concepts/schemas.mdx) and [Validation Rules](../core- ## 2. Validate data Call `safeParse()` with the data you received — for example -`jsonDecode(response.body)`. It returns a `SchemaResult` for invalid input and -for recoverable `Exception` values thrown by constraint/refinement callbacks. -`Error` values from those callbacks are rethrown with their original stack -trace. Codec/transform decoders and `safeParseAs` mappers instead turn thrown -values, including `Error` values, into `SchemaTransformError` failures. +`jsonDecode(response.body)`. It returns a `SchemaResult`, so expected validation +failures stay in normal control flow instead of throwing. For callback, +transform, and codec failure semantics, see [Error Handling](../core-concepts/error-handling.mdx). ```dart final result = userSchema.safeParse({'name': 'Alice', 'age': 30}); diff --git a/docs/guides/common-recipes.mdx b/docs/guides/common-recipes.mdx index 5e5069ef..0c0a449a 100644 --- a/docs/guides/common-recipes.mdx +++ b/docs/guides/common-recipes.mdx @@ -2,7 +2,9 @@ title: Common Recipes --- -Quick solutions to common validation scenarios. Each recipe is a complete, working example you can copy and adapt. +Quick solutions to common validation scenarios. Each recipe shows a working +schema pattern you can copy and adapt. Examples assume +`package:ack/ack.dart` is imported unless the snippet shows other imports. ## Email and password validation @@ -50,7 +52,7 @@ Validate nested data structures like addresses: final addressSchema = Ack.object({ 'street': Ack.string().notEmpty(), 'city': Ack.string().notEmpty(), - 'zipCode': Ack.string().matches(r'\d{5}(-\d{4})?'), + 'zipCode': Ack.string().matches(r'^\d{5}(-\d{4})?$'), 'country': Ack.string().notEmpty(), }); @@ -158,9 +160,18 @@ final registrationSchema = Ack.object({ ## API response validation -Validate external API responses before using the payload: +Validate external API responses before using the payload. APIs commonly add +fields over time, so this boundary schema validates the fields the application +uses while allowing the rest of GitHub's response to pass through. + +Add the HTTP client first with `dart pub add http`. ```dart +import 'dart:convert'; + +import 'package:ack/ack.dart'; +import 'package:http/http.dart' as http; + // GitHub user API response final githubUserSchema = Ack.object({ 'login': Ack.string(), @@ -172,8 +183,8 @@ final githubUserSchema = Ack.object({ 'public_repos': Ack.integer(), 'followers': Ack.integer(), 'following': Ack.integer(), - 'created_at': Ack.string(), // ISO datetime -}); + 'created_at': Ack.string().datetime(), +}, additionalProperties: true); // Usage in API call Future fetchUser(String username) async { @@ -181,11 +192,15 @@ Future fetchUser(String username) async { Uri.parse('https://api.github.com/users/$username'), ); + if (response.statusCode != 200) { + throw StateError('GitHub request failed: ${response.statusCode}'); + } + final json = jsonDecode(response.body); final result = githubUserSchema.safeParse(json); if (result.isOk) { - final user = result.getOrThrow(); + final user = result.getOrThrow()!; print('User: ${user['login']}'); } else { print('Invalid API response: ${result.getError()}'); diff --git a/docs/guides/creating-schema-converter-packages.md b/docs/guides/creating-schema-converter-packages.mdx similarity index 99% rename from docs/guides/creating-schema-converter-packages.md rename to docs/guides/creating-schema-converter-packages.mdx index 4aec863a..4136d3bf 100644 --- a/docs/guides/creating-schema-converter-packages.md +++ b/docs/guides/creating-schema-converter-packages.mdx @@ -1,4 +1,6 @@ -# Creating Schema Converter Packages for Ack +--- +title: Creating Adapter Packages +--- This guide provides detailed instructions for creating schema converter packages that transform Ack validation schemas into other schema formats (e.g., JSON Schema, OpenAPI, GraphQL, Protobuf, TypeBox, AJV, etc.). diff --git a/docs/guides/custom-validation.mdx b/docs/guides/custom-validation.mdx index 9b9802fc..085d72e1 100644 --- a/docs/guides/custom-validation.mdx +++ b/docs/guides/custom-validation.mdx @@ -34,7 +34,7 @@ class IsPositiveConstraint extends Constraint with Validator { final priceSchema = Ack.double().constrain(IsPositiveConstraint()); print(priceSchema.safeParse(10.5).isOk); // true -print(priceSchema.safeParse(-5).isFail); // true +print(priceSchema.safeParse(-5.0).isFail); // true ``` ## Cross-field rules with `.refine()` @@ -65,7 +65,7 @@ The optional `message` parameter on `.constrain()` lets you customize the failur final schema = Ack.double() .constrain(IsPositiveConstraint(), message: 'Price must be greater than zero.'); -final result = schema.safeParse(-10); +final result = schema.safeParse(-10.0); if (result.isFail) { print(result.getError().message); // Price must be greater than zero. } diff --git a/docs/guides/flutter-form-validation.mdx b/docs/guides/flutter-form-validation.mdx index d7052fca..a4976c8e 100644 --- a/docs/guides/flutter-form-validation.mdx +++ b/docs/guides/flutter-form-validation.mdx @@ -47,7 +47,7 @@ class _SignUpFormState extends State { final _usernameSchema = Ack.string() .minLength(3) .maxLength(20) - .matches(r'[a-zA-Z0-9_]+') + .matches(r'^[a-zA-Z0-9_]+$') .notEmpty(); final _emailSchema = Ack.string() diff --git a/docs/guides/json-schema-integration.mdx b/docs/guides/json-schema-integration.mdx index 7a72f07f..076a963d 100644 --- a/docs/guides/json-schema-integration.mdx +++ b/docs/guides/json-schema-integration.mdx @@ -9,18 +9,23 @@ Your Ack [schema](../core-concepts/schemas.mdx) already describes your data's sh 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'; import 'dart:convert'; -// Example User Schema +import 'package:ack/ack.dart'; + +enum UserRole { admin, user, guest } + final userSchema = Ack.object({ 'id': Ack.integer().positive().describe('Unique user identifier'), - 'name': Ack.string().minLength(2).maxLength(50).describe('User\'s full name'), - 'email': Ack.string().email().describe('User\'s email address'), + 'name': Ack.string() + .minLength(2) + .maxLength(50) + .describe("User's full name"), + 'email': Ack.string().email().describe("User's email address"), 'role': Ack.enumValues(UserRole.values).withDefault(UserRole.user), 'isActive': Ack.boolean().withDefault(true), 'tags': Ack.list(Ack.string()).unique().describe('List of user tags').nullable(), - 'age': Ack.integer().min(0).max(120).nullable().describe('User\'s age'), + 'age': Ack.integer().min(0).max(120).nullable().describe("User's age"), }).describe('Represents a user in the system'); void main() { @@ -33,7 +38,12 @@ 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 the [adapter authoring guide on GitHub](https://github.com/btwld/ack/blob/main/docs/guides/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. Start with the [adapter quickstart](./schema-converter-quickstart.mdx), +> then use the [complete adapter authoring guide](./creating-schema-converter-packages.mdx) +> for architecture and testing guidance. ## Adapter model @@ -64,14 +74,14 @@ for (final warning in model.warnings) { }, "name": { "type": "string", - "description": "User\'s full name", + "description": "User's full name", "minLength": 2, "maxLength": 50 }, "email": { "type": "string", "format": "email", - "description": "User\'s email address" + "description": "User's email address" }, "role": { "type": "string", @@ -105,7 +115,7 @@ for (final warning in model.warnings) { "anyOf": [ { "type": "integer", - "description": "User\'s age", + "description": "User's age", "minimum": 0, "maximum": 120 }, @@ -299,6 +309,8 @@ final jsonSchema = userSchema.toJsonSchema(); Schemas with default values include them in the generated JSON Schema: ```dart +enum Theme { light, dark } + final configSchema = Ack.object({ 'theme': Ack.enumValues(Theme.values).withDefault(Theme.light), 'notifications': Ack.boolean().withDefault(true), diff --git a/docs/guides/schema-converter-quickstart.md b/docs/guides/schema-converter-quickstart.mdx similarity index 99% rename from docs/guides/schema-converter-quickstart.md rename to docs/guides/schema-converter-quickstart.mdx index c9b7e582..ff0a52f1 100644 --- a/docs/guides/schema-converter-quickstart.md +++ b/docs/guides/schema-converter-quickstart.mdx @@ -1,4 +1,6 @@ -# Schema Converter Package - Quick Start Template +--- +title: Adapter Package Quickstart +--- **Use this for rapid prototyping of new schema converter packages** @@ -386,4 +388,4 @@ dart format . ## Reference -See [Creating Schema Converter Packages](./creating-schema-converter-packages.md) for detailed guidance. +See [Creating Schema Converter Packages](./creating-schema-converter-packages.mdx) for detailed guidance. diff --git a/docs/index.mdx b/docs/index.mdx index 6dddde0e..ae64aad4 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -61,6 +61,6 @@ New here? The [Quickstart Tutorial](/getting-started/quickstart-tutorial) takes - [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 +- [API Reference](/api-reference/) — a core API quick reference with a link to generated API docs *Building with an AI agent? Start at [`/llms.txt`](/llms.txt) for a compact, machine-readable index.* diff --git a/packages/ack/test/documentation/core_concepts_json_serialization_examples_test.dart b/packages/ack/test/documentation/core_concepts_json_serialization_examples_test.dart index 57f75fb0..bef463d2 100644 --- a/packages/ack/test/documentation/core_concepts_json_serialization_examples_test.dart +++ b/packages/ack/test/documentation/core_concepts_json_serialization_examples_test.dart @@ -65,6 +65,38 @@ void main() { expect(age, equals(30)); }); + test('validated JSON-native data can be encoded', () { + final result = buildUserSchema().safeParse({ + 'name': 'Alice', + 'age': 30, + 'email': 'alice@example.com', + }); + + final validData = result.getOrThrow()!; + final encoded = jsonEncode(validData); + + expect(jsonDecode(encoded), equals(validData)); + }); + + test('codec runtime values are encoded to their JSON boundary', () { + final eventSchema = Ack.object({ + 'name': Ack.string(), + 'startsAt': Ack.datetime(), + }); + final event = eventSchema.parse({ + 'name': 'Launch', + 'startsAt': '2026-01-15T14:00:00Z', + }); + + final boundaryData = eventSchema.encode(event); + final encoded = jsonEncode(boundaryData); + + expect( + jsonDecode(encoded), + equals({'name': 'Launch', 'startsAt': '2026-01-15T14:00:00.000Z'}), + ); + }); + test('manual schema example mirrors generated schema usage', () { final generatedUserSchema = Ack.object({ 'name': Ack.string(), diff --git a/packages/ack/test/documentation/core_concepts_schemas_examples_test.dart b/packages/ack/test/documentation/core_concepts_schemas_examples_test.dart index eb66ea8d..b6df23b8 100644 --- a/packages/ack/test/documentation/core_concepts_schemas_examples_test.dart +++ b/packages/ack/test/documentation/core_concepts_schemas_examples_test.dart @@ -32,6 +32,7 @@ void main() { .matches(r'^[a-zA-Z0-9_]+$'); expect(usernameSchema.safeParse('user_123').isOk, isTrue); expect(usernameSchema.safeParse('u').isFail, isTrue); + expect(usernameSchema.safeParse('user_123!').isFail, isTrue); final emailSchema = Ack.string().email(); expect(emailSchema.safeParse('valid@example.com').isOk, isTrue); @@ -122,6 +123,17 @@ void main() { }); expect(result.isOk, isTrue); + expect( + userSchema.safeParse({ + 'name': 'Jane', + 'address': { + 'street': '123 Main St', + 'city': 'Springfield', + 'zipCode': '12345-extra', + }, + }).isFail, + isTrue, + ); final data = result.getOrThrow()!; final address = data['address'] as Map; expect(address['city'], equals('Springfield')); @@ -424,6 +436,7 @@ void main() { final parsedDate = dateSchema.safeParse('2024-01-01').getOrThrow() as DateTime; expect(parsedDate.year, equals(2024)); + expect(dateSchema.safeParse('date:2024-01-01').isFail, isTrue); }); }); }); diff --git a/packages/ack/test/documentation/core_concepts_validation_examples_test.dart b/packages/ack/test/documentation/core_concepts_validation_examples_test.dart index 4602139c..36ff0ae7 100644 --- a/packages/ack/test/documentation/core_concepts_validation_examples_test.dart +++ b/packages/ack/test/documentation/core_concepts_validation_examples_test.dart @@ -154,11 +154,12 @@ void main() { final usernameSchema = Ack.string() .minLength(3) .maxLength(20) - .matches(r'[a-z0-9_]+') + .matches(r'^[a-z0-9_]+$') .notEmpty(); expect(usernameSchema.safeParse('user_name').isOk, isTrue); expect(usernameSchema.safeParse('UPPER').isFail, isTrue); + expect(usernameSchema.safeParse('user-name').isFail, isTrue); }); test('numeric constraint chaining', () { diff --git a/packages/ack/test/documentation/documentation_source_integrity_test.dart b/packages/ack/test/documentation/documentation_source_integrity_test.dart new file mode 100644 index 00000000..658cdcae --- /dev/null +++ b/packages/ack/test/documentation/documentation_source_integrity_test.dart @@ -0,0 +1,141 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:test/test.dart'; + +String _readFromRepo(String path) => File('../../$path').readAsStringSync(); + +void main() { + group('documentation source integrity', () { + test( + 'full-value regex examples are anchored in their literal snippets', + () { + final commonRecipes = _readFromRepo('docs/guides/common-recipes.mdx'); + final flutterForms = _readFromRepo( + 'docs/guides/flutter-form-validation.mdx', + ); + final validation = _readFromRepo('docs/core-concepts/validation.mdx'); + final schemas = _readFromRepo('docs/core-concepts/schemas.mdx'); + + expect(commonRecipes, contains(".matches(r'^\\d{5}(-\\d{4})?\$')")); + expect(flutterForms, contains(".matches(r'^[a-zA-Z0-9_]+\$')")); + expect(validation, contains(".matches(r'^[a-z0-9_]+\$')")); + expect(schemas, contains(".matches(r'^[a-zA-Z0-9_]+\$')")); + expect(schemas, contains(".matches(r'^\\d{5}\$')")); + expect(schemas, contains(".matches(r'^\\d{4}-\\d{2}-\\d{2}\$')")); + }, + ); + + test('double-schema examples use double inputs', () { + final customValidation = _readFromRepo( + 'docs/guides/custom-validation.mdx', + ); + + expect(customValidation, contains('safeParse(-5.0)')); + expect(customValidation, contains('safeParse(-10.0)')); + }); + + test('GitHub response recipe permits undocumented response fields', () { + final commonRecipes = _readFromRepo('docs/guides/common-recipes.mdx'); + final githubRecipe = RegExp( + r'final githubUserSchema = Ack\.object\([\s\S]*?Future fetchUser', + ).firstMatch(commonRecipes)?.group(0); + + expect(githubRecipe, isNotNull); + expect(githubRecipe, contains('additionalProperties: true')); + }); + + test('JSON serialization covers the outbound JSON path', () { + final serialization = _readFromRepo( + 'docs/core-concepts/json-serialization.mdx', + ); + + expect(serialization, contains('## Encoding validated data')); + expect(serialization, contains('jsonEncode(validData)')); + expect(serialization, contains('[Codecs](./codecs.mdx)')); + }); + + test('primary JSON Schema example is self-contained and valid JSON', () { + final guide = _readFromRepo('docs/guides/json-schema-integration.mdx'); + + expect(guide, contains('enum UserRole { admin, user, guest }')); + expect(guide, contains('enum Theme { light, dark }')); + + final output = RegExp( + r'\*\*Output JSON \(JSON Schema Object\):\*\*\s*```json\s*([\s\S]*?)```', + ).firstMatch(guide)?.group(1); + expect(output, isNotNull); + expect(() => jsonDecode(output!), returnsNormally); + }); + + test('date converter README uses the codec runtime invariant', () { + final readme = _readFromRepo( + 'packages/ack_json_schema_builder/README.md', + ); + + expect(readme, contains('Ack.date().min(DateTime(2026))')); + expect(readme, isNot(contains('Ack.date().min(DateTime.utc(2026))'))); + }); + + test('converter authoring guides are published and navigable', () { + final docsConfig = jsonDecode(_readFromRepo('docs.json')) as Object?; + final serializedConfig = jsonEncode(docsConfig); + + expect( + File('../../docs/guides/schema-converter-quickstart.mdx').existsSync(), + isTrue, + ); + expect( + File( + '../../docs/guides/creating-schema-converter-packages.mdx', + ).existsSync(), + isTrue, + ); + expect(serializedConfig, contains('/guides/schema-converter-quickstart')); + expect( + serializedConfig, + contains('/guides/creating-schema-converter-packages'), + ); + }); + + test('community health files provide contribution routes', () { + for (final path in ['CODE_OF_CONDUCT.md', 'SECURITY.md', 'SUPPORT.md']) { + expect(File('../../$path').existsSync(), isTrue, reason: path); + } + }); + + test('quickstart delegates advanced failure semantics', () { + final quickstart = _readFromRepo( + 'docs/getting-started/quickstart-tutorial.mdx', + ); + + expect( + quickstart, + isNot(contains('`Error` values from those callbacks are rethrown')), + ); + expect(quickstart, contains('[Error Handling]')); + }); + + test('configuration documents the main schema-level choices', () { + final configuration = _readFromRepo( + 'docs/core-concepts/configuration.mdx', + ); + + expect(configuration, contains('### Optional and nullable values')); + expect(configuration, contains('### Default values')); + expect(configuration, contains('### Schema metadata')); + expect(configuration, contains('message:')); + }); + + test('API page distinguishes quick reference from exhaustive API docs', () { + final apiReference = _readFromRepo('docs/api-reference/index.mdx'); + + expect(apiReference, contains('toSchemaModel()')); + expect( + apiReference, + contains('https://pub.dev/documentation/ack/latest/ack/'), + ); + expect(apiReference, contains('generated API documentation')); + }); + }); +} diff --git a/packages/ack/test/documentation/guides_common_recipes_examples_test.dart b/packages/ack/test/documentation/guides_common_recipes_examples_test.dart index 50d16687..e7306dda 100644 --- a/packages/ack/test/documentation/guides_common_recipes_examples_test.dart +++ b/packages/ack/test/documentation/guides_common_recipes_examples_test.dart @@ -210,8 +210,8 @@ void main() { 'public_repos': Ack.integer(), 'followers': Ack.integer(), 'following': Ack.integer(), - 'created_at': Ack.string(), - }); + 'created_at': Ack.string().datetime(), + }, additionalProperties: true); final sampleResponse = jsonEncode({ 'login': 'octocat', @@ -224,6 +224,7 @@ void main() { 'followers': 100, 'following': 10, 'created_at': '2011-01-25T18:44:36Z', + 'company': '@github', }); final json = jsonDecode(sampleResponse); @@ -231,6 +232,7 @@ void main() { expect(result.isOk, isTrue); final user = result.getOrThrow()!; expect(user['login'], equals('octocat')); + expect(user['company'], equals('@github')); }); }); } diff --git a/packages/ack_json_schema_builder/README.md b/packages/ack_json_schema_builder/README.md index dd598962..4402d40d 100644 --- a/packages/ack_json_schema_builder/README.md +++ b/packages/ack_json_schema_builder/README.md @@ -63,10 +63,13 @@ the `json_schema_builder` schema. If a downstream validator or consumer ignores a JSON Schema keyword, validate with ACK after parsing. ```dart -final schema = Ack.date().min(DateTime.utc(2026)); +final schema = Ack.date().min(DateTime(2026)); final jsonSchema = schema.toJsonSchemaBuilder(); // Includes format: date. ``` +`Ack.date()` uses local-midnight `DateTime` values at runtime. Use +`DateTime.utc(...)` with `Ack.datetime()`, whose runtime invariant is UTC. + ## Usage ```dart