Skip to content
Open
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 .fvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"flutter": "3.44.4"
}
50 changes: 28 additions & 22 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -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
Expand All @@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ build/
pubspec.lock
coverage/
**/pubspec_overrides.yaml

# FVM Version Cache
.fvm/
10 changes: 5 additions & 5 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions .idea/runConfigurations/melos_run_format_ci.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"dart.flutterSdkPath": ".fvm/versions/3.44.4"
}
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, E>` alias for `FormField<T, T, E>`
- 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))
Expand Down
50 changes: 36 additions & 14 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -26,5 +48,5 @@ analyzer:
missing_return: error
missing_required_param: error
exclude:
- "build/**"
- "**/*.g.dart"
- 'build/**'
- '**/*.g.dart'
7 changes: 1 addition & 6 deletions cspell.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
{
"words": [
"Bodyless",
"pana",
"webp",
"writeln"
]
"words": ["Bodyless", "pana", "webp", "writeln", "nullable"]
}
81 changes: 81 additions & 0 deletions docs/migrations/0.0.1-to-0.1.0.md
Original file line number Diff line number Diff line change
@@ -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<ExampleFormErrors> {}
class ExampleFormErrors extends FormErrors<ExampleFormBody> {}

// 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<String?>(name, isRequired: true),
age: GenericFormField<int?>(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<String?>(name, isRequired: true),
age: GenericFormField<String>(age),
);
}

// After
factory ExampleFormBody({
@FieldRequired() String? name,
required String age,
}) = _$ExampleFormBody;
```

## 4) `SimpleFormField<T, E>` alias
Use `SimpleFormField` when raw and parsed types are the same.

```dart
// Before
class NonEmptyNameField extends FormField<String, String, NameError> {
const NonEmptyNameField(super.rawValue);
@override
String get value => rawValue;
@override
NameError? validate() => value.isEmpty ? NameError.empty : null;
}

// After
class NonEmptyNameField extends SimpleFormField<String, NameError> {
const NonEmptyNameField(super.rawValue);
@override
String get value => rawValue;
@override
NameError? validate() => value.isEmpty ? NameError.empty : null;
}
```
31 changes: 14 additions & 17 deletions packages/shape/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -43,39 +44,35 @@ 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() {
final formBody = ExampleFormBody();
}
```

### 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.
Expand Down Expand Up @@ -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).
13 changes: 4 additions & 9 deletions packages/shape/example/lib/example_form_body.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExampleFormErrors>
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;
}
Loading
Loading