diff --git a/.fvmrc b/.fvmrc
new file mode 100644
index 0000000..5913bec
--- /dev/null
+++ b/.fvmrc
@@ -0,0 +1,3 @@
+{
+ "flutter": "3.44.4"
+}
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c097c5d..98da604 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,7 +7,11 @@ on:
jobs:
semantic_pull_request:
- uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@fb76d33002e1a5c6bc7c0ee9ba2b5ff2d9908a00 # v1.17
+ runs-on: ubuntu-latest
+ steps:
+ - uses: amannn/action-semantic-pull-request@e32d7e603df1aa1ba07e981f2a23455dee596825 # v5
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build:
runs-on: ubuntu-latest
@@ -35,7 +39,7 @@ jobs:
run: |
dart pub global activate coverage
melos exec --dir-exists="test" -- "dart test --coverage=coverage"
- melos exec --dir-exists="test" -- "dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --report-on=lib"
+ melos exec --dir-exists="test" -- "dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --package=. --report-on=lib"
- name: Upload Artifacts
uses: actions/upload-artifact@v4
@@ -46,21 +50,14 @@ jobs:
- name: Generate Coverage Matrix
id: coverage-matrix
run: |
- # Get packages that have tests
- packages=$(melos list --dir-exists="test")
-
- # Create JSON array of objects with name and path
- matrix_json="["
- first=true
- for package in $packages; do
- if [ "$first" = true ]; then
- first=false
- else
- matrix_json+=","
- fi
- matrix_json+="{\"name\":\"$package\",\"path\":\"packages/$package/coverage/lcov.info\"}"
- done
- matrix_json+="]"
+ matrix_json="$(melos list --dir-exists="test" --json | jq -c --arg root "$PWD" '
+ [.[] | {
+ name: .name,
+ private: .private,
+ directory: (.location | ltrimstr($root + "/")),
+ path: ((.location | ltrimstr($root + "/")) + "/coverage/lcov.info")
+ }]
+ ')"
echo "matrix=$matrix_json" >> "$GITHUB_OUTPUT"
echo "Generated coverage matrix: $matrix_json"
@@ -81,13 +78,12 @@ jobs:
name: coverage
path: packages/
- - name: Inspect directories
- run: ls -R packages/
-
- name: Check Code Coverage
+ if: ${{ !matrix.private }}
uses: VeryGoodOpenSource/very_good_coverage@c953fca3e24a915e111cc6f55f03f756dcb3964c # v3
with:
path: ${{ matrix.path }}
+ min_coverage: 80
check_pana:
needs: build
@@ -111,6 +107,16 @@ jobs:
run: dart pub global activate pana
- name: Verify Pub Score
+ if: ${{ !matrix.private }}
run: |
- cd "packages/${{ matrix.name }}" || exit 1
- ../../tool/verify_pub_score.sh "$(cat PANA_SCORE)"
+ MIN_SCORE=""
+ if [ -f "${{ matrix.directory }}/PANA_SCORE" ]; then
+ MIN_SCORE="$(cat "${{ matrix.directory }}/PANA_SCORE")"
+ fi
+
+ if [ "${{ matrix.name }}" = "shape" ]; then
+ cd "${{ matrix.directory }}" || exit
+ ../../tool/verify_pub_score.sh "$MIN_SCORE"
+ else
+ tool/verify_pub_score_workspace.sh "$MIN_SCORE" "${{ matrix.directory }}"
+ fi
diff --git a/.gitignore b/.gitignore
index 0d25ed4..a4a44e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,6 @@ build/
pubspec.lock
coverage/
**/pubspec_overrides.yaml
+
+# FVM Version Cache
+.fvm/
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
index f7b8e45..43834eb 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -2,11 +2,11 @@
-
-
-
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/melos_run_format_ci.xml b/.idea/runConfigurations/melos_run_format_ci.xml
new file mode 100644
index 0000000..284fc60
--- /dev/null
+++ b/.idea/runConfigurations/melos_run_format_ci.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..a7b6726
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "dart.flutterSdkPath": ".fvm/versions/3.44.4"
+}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f7ffacc..bff9236 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,19 @@
All notable changes to this project will be documented in this file.
+### 0.1.0 - 2026-08-06
+
+- feat: simplify API surface and implementation details
+ - remove `FormBody` and `FormErrors` generics
+ - add `SimpleFormField` alias for `FormField`
+ - add `@FieldRequired()` for optional params that must validate as required
+ - support redirecting factories (`factory Foo(...) = _$Foo`) in generator
+ - add auto-wrap for plain factory params via `GenericFormField`
+ - remove `Equatable` usage in favor of explicit `operator ==` / `hashCode` for bodies and errors
+ - improve constructor/field validation diagnostics in generator
+- chore: upgrade Dart version constraint and dependencies
+- test: update and improve tests
+
### 0.0.2 - 2023-08-11 (`shape_generator` only)
- chore(shape_generator): relax dependency constraint on `analyzer` to maximize compatibility with Flutter projects ([#10](https://github.com/Betterment/shape/pull/10))
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 7140411..fd19b19 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -5,18 +5,40 @@ formatter:
linter:
rules:
- - cascade_invocations
- - lines_longer_than_80_chars
- - package_api_docs
- - prefer_const_constructors
- - prefer_const_constructors_in_immutables
- - prefer_const_declarations
- - prefer_const_literals_to_create_immutables
- - prefer_single_quotes
- - public_member_api_docs
- - require_trailing_commas
- - sort_constructors_first
- - sort_pub_dependencies
+ cascade_invocations: true
+ lines_longer_than_80_chars: true
+ prefer_const_constructors: true
+ prefer_const_constructors_in_immutables: true
+ prefer_const_declarations: true
+ prefer_const_literals_to_create_immutables: true
+ prefer_single_quotes: true
+ public_member_api_docs: true
+ require_trailing_commas: true
+ sort_constructors_first: true
+ sort_pub_dependencies: true
+ simple_directive_paths: true
+ simplify_variable_pattern: true
+ remove_deprecations_in_breaking_versions: true
+ switch_on_type: true
+ unnecessary_unawaited: true
+ use_null_aware_elements: true
+ unnecessary_ignore: true
+ strict_top_level_inference: true
+ unnecessary_underscores: true
+ omit_obvious_property_types: true
+ unnecessary_async: true
+ unsafe_variance: true
+ use_truncating_division: true
+ omit_obvious_local_variable_types: true
+ avoid_futureor_void: true
+ unintended_html_in_doc_comment: true
+ invalid_runtime_check_with_js_interop_types: true
+ document_ignores: true
+ unnecessary_library_name: true
+ missing_code_block_language_in_doc_comment: true
+ annotate_redeclares: true
+ no_self_assignments: true
+ no_wildcard_variable_uses: true
analyzer:
language:
@@ -26,5 +48,5 @@ analyzer:
missing_return: error
missing_required_param: error
exclude:
- - "build/**"
- - "**/*.g.dart"
+ - 'build/**'
+ - '**/*.g.dart'
diff --git a/cspell.json b/cspell.json
index 74cabdb..82ca554 100644
--- a/cspell.json
+++ b/cspell.json
@@ -1,8 +1,3 @@
{
- "words": [
- "Bodyless",
- "pana",
- "webp",
- "writeln"
- ]
+ "words": ["Bodyless", "pana", "webp", "writeln", "nullable"]
}
diff --git a/docs/migrations/0.0.1-to-0.1.0.md b/docs/migrations/0.0.1-to-0.1.0.md
new file mode 100644
index 0000000..cc9c7d0
--- /dev/null
+++ b/docs/migrations/0.0.1-to-0.1.0.md
@@ -0,0 +1,81 @@
+# 0.0.1 → 0.1.0
+
+## 1) `FormBody` / `FormErrors` generics removed
+Remove type args from the base classes.
+
+```dart
+// Before
+abstract class ExampleFormBody extends FormBody {}
+class ExampleFormErrors extends FormErrors {}
+
+// After
+abstract class ExampleFormBody extends FormBody {}
+class ExampleFormErrors extends FormErrors {}
+```
+
+## 2) New `@FieldRequired()` annotation
+If a factory param is optional but should validate as required, add `@FieldRequired()`.
+
+```dart
+// Before
+factory ExampleFormBody({
+ String? name,
+ int? age,
+}) {
+ return _$ExampleFormBody(
+ name: GenericFormField(name, isRequired: true),
+ age: GenericFormField(age),
+ );
+}
+
+// After
+factory ExampleFormBody({
+ @FieldRequired() String? name,
+ int? age,
+}) = _$ExampleFormBody;
+```
+
+## 3) Factory body can be simplified
+For plain values, you can now use a redirecting factory and let generation auto-wrap.
+
+```dart
+// Before
+factory ExampleFormBody({
+ required String? name,
+ required String age,
+}) {
+ return _$ExampleFormBody(
+ name: GenericFormField(name, isRequired: true),
+ age: GenericFormField(age),
+ );
+}
+
+// After
+factory ExampleFormBody({
+ @FieldRequired() String? name,
+ required String age,
+}) = _$ExampleFormBody;
+```
+
+## 4) `SimpleFormField` alias
+Use `SimpleFormField` when raw and parsed types are the same.
+
+```dart
+// Before
+class NonEmptyNameField extends FormField {
+ const NonEmptyNameField(super.rawValue);
+ @override
+ String get value => rawValue;
+ @override
+ NameError? validate() => value.isEmpty ? NameError.empty : null;
+}
+
+// After
+class NonEmptyNameField extends SimpleFormField {
+ const NonEmptyNameField(super.rawValue);
+ @override
+ String get value => rawValue;
+ @override
+ NameError? validate() => value.isEmpty ? NameError.empty : null;
+}
+```
diff --git a/packages/shape/README.md b/packages/shape/README.md
index 0569de9..2804866 100644
--- a/packages/shape/README.md
+++ b/packages/shape/README.md
@@ -8,6 +8,7 @@ A package for building forms that can be easily reused, validated, and parsed, p
- [Table of Contents](#table-of-contents)
- [Summary](#summary)
- [Usage](#usage)
+ - [Migrations](#migrations)
- [Principle](#principle)
- [Features](#features)
- [Access parsed values](#access-parsed-values)
@@ -43,32 +44,24 @@ To generate a form body, in this case called `ExampleFormBody`;
1. Create an abstract class `ExampleFormBody` annotated with `@GenerateFormBody()`.
2. Add the `_$ExampleFormBodyFields` mixin.
-3. Create a single unnamed factory that returns an instance of `_$ExampleFormBody` containing all form fields that should be present in the form body. All parameters must be an instance of a class that extends `FormField`, a class provided by this package.
+3. Add a private empty constructor (`const ExampleFormBody._();`) and one unnamed factory that returns `_$ExampleFormBody`.
A full example might look like this:
```dart
import 'package:shape/shape.dart';
-import 'package:shape_addons/shape_addons.dart';
+import 'package:shape_starter_kit/shape_starter_kit.dart';
part 'example_form_body.g.dart';
@GenerateFormBody()
-abstract class ExampleFormBody with _$ExampleFormBodyFields {
+abstract class ExampleFormBody extends FormBody with _$ExampleFormBodyFields {
+ const ExampleFormBody._();
+
factory ExampleFormBody({
- required String? foo,
- required String? bar,
- }) {
- return _$ExampleFormBody(
- name: GenericFormField(
- value: foo,
- isRequired: true,
- ),
- otherName: RangedDoubleFormField(
- value: bar,
- ),
- );
- }
+ @FieldRequired() String? foo,
+ int? bar,
+ }) = _$ExampleFormBody;
}
void main() {
@@ -76,6 +69,10 @@ void main() {
}
```
+### Migrations
+
+- [0.0.1 → 0.1.0](docs/migrations/0.0.1-to-0.1.0.md)
+
### Principle
Shape works by separating form fields, bodies, validation logic and parsing logic into separate classes.
@@ -135,7 +132,7 @@ To run the example, run `build_runner` in [the `example` folder](https://github.
```shell
cd example
-flutter pub run build_runner build --delete-conflicting-outputs
+flutter pub run build_runner build
```
A new form body will be generated based on the contents of [`example/lib/example_form_body.dart`](https://github.com/betterment/shape/tree/main/packages/shape/example/lib/example_form_body.dart). After the code generator has completed, examine the contents of the file [`example/lib/example_form_body.g.dart`](https://github.com/betterment/shape/tree/main/packages/shape/example/lib/example_form_body.g.dart).
diff --git a/packages/shape/example/lib/example_form_body.dart b/packages/shape/example/lib/example_form_body.dart
index 0cddf26..7c3e9e4 100644
--- a/packages/shape/example/lib/example_form_body.dart
+++ b/packages/shape/example/lib/example_form_body.dart
@@ -4,14 +4,9 @@ import 'package:shape_starter_kit/shape_starter_kit.dart';
part 'example_form_body.g.dart';
@GenerateFormBody()
-abstract class ExampleFormBody extends FormBody
- with _$ExampleFormBodyFields {
- factory ExampleFormBody({required String? name, int? age}) {
- return _$ExampleFormBody(
- name: GenericFormField(name, isRequired: true),
- age: GenericFormField(age),
- );
- }
-
+abstract class ExampleFormBody extends FormBody with _$ExampleFormBodyFields {
const ExampleFormBody._();
+
+ factory ExampleFormBody({@FieldRequired() String? name, int? age}) =
+ _$ExampleFormBody;
}
diff --git a/packages/shape/example/lib/example_form_body.g.dart b/packages/shape/example/lib/example_form_body.g.dart
index 61f4ebe..02e4147 100644
--- a/packages/shape/example/lib/example_form_body.g.dart
+++ b/packages/shape/example/lib/example_form_body.g.dart
@@ -6,20 +6,17 @@ part of 'example_form_body.dart';
// ShapeGenerator
// **************************************************************************
+// ignore_for_file: unused_element, cast_nullable_to_non_nullable, prefer_const_constructors_in_immutables
// Form Body "_$ExampleFormBody"
@immutable
-class _$ExampleFormBody extends ExampleFormBody
- with _$ExampleFormBodyFields, EquatableMixin {
- factory _$ExampleFormBody({
- required GenericFormField name,
- required GenericFormField age,
- }) {
- return _$ExampleFormBody._(name, age);
+class _$ExampleFormBody extends ExampleFormBody {
+ factory _$ExampleFormBody({String? name, int? age}) {
+ return _$ExampleFormBody._(
+ GenericFormField(name, isRequired: true),
+ GenericFormField(age),
+ );
}
- const _$ExampleFormBody._(
- this._name,
- this._age,
- ) : super._();
+ const _$ExampleFormBody._(this._name, this._age) : super._();
@override
final GenericFormField _name;
@override
@@ -30,36 +27,30 @@ class _$ExampleFormBody extends ExampleFormBody
int? get age => _age.value;
@override
ExampleFormErrors validate() {
- return ExampleFormErrors(
- name: _name.validate(),
- age: _age.validate(),
- );
+ return ExampleFormErrors(name: _name.validate(), age: _age.validate());
}
@override
_$ExampleFormBodyCopyWith get copyWith => _$ExampleFormBodyCopyWithImpl(this);
@override
- List