Compare commits

..
Author SHA1 Message Date
hugo d6bb3c2ac7 chore(release): publish packages
- wyatt_form_bloc@0.0.4
 - wyatt_authentication_bloc@0.2.1+2
2022-08-25 17:36:44 +02:00
hugo 5515c0e951 chore(release): publish packages
- wyatt_type_utils@0.0.2
2022-07-18 16:58:32 +02:00
hugo daf8ec27ab Merge pull request 'docs: remove useless web example' (#14) from types/clean_example_web into master
Reviewed-on: #14
2022-07-18 14:57:09 +00:00
hugo ec5e65ac82 docs: remove useless web example 2022-07-18 16:56:48 +02:00
hugo d7e618e04c Merge pull request 'types/new_package' (#13) from types/new_package into master
Reviewed-on: #13
2022-07-18 14:55:21 +00:00
hugo fc7a4560f0 feat(types): add strongly typed Option and Result. 2022-07-18 16:54:38 +02:00
hugo 52665cdee5 feat(types): add mason generated package 2022-07-17 22:29:36 +02:00
hugo 5300e684de Merge pull request 'feat(from): add form validator class with strategy design pattern' (#12) from form/feature/form_validator_class into master
Reviewed-on: #12
2022-07-15 16:24:33 +00:00
hugo 607b986848 feat(from): add form validator class with strategy design pattern 2022-07-14 16:59:24 +02:00
Malo Léon 61c6e637ec doc(bloc_helper): update readme 2022-07-14 11:55:49 +01:00
Malo Léon efda077f4b feat(bloc_helper): add wrap feature 2022-07-14 11:45:47 +01:00
Malo Léon e00f4f55b7 refactor(bloc_helper): general code update 2022-07-14 11:11:02 +01:00
hugo 58fec4bf48 chore(release): publish packages
- wyatt_analysis@2.2.1
2022-07-12 12:20:15 +02:00
hugo dd380a71db fix: import right version 2022-07-12 12:20:07 +02:00
hugo 0d130ce84a chore(release): publish packages
- wyatt_analysis@2.2.0
2022-07-11 23:54:57 +02:00
hugo 28d9aa69e7 fix: rename to follow melos versioning and remove redundant rule 2022-07-11 23:54:48 +02:00
hugo 45e737dde2 feat: promote experimental, update analyzer rules and add some linter rules 2022-07-11 23:52:37 +02:00
hugo 2d1826235f chore: update gitignore 2022-07-10 22:25:42 +02:00
hugo f558685f11 chore(release): publish packages
- form_bloc_example@1.1.0
 - wyatt_form_bloc@0.0.3
 - authentication_bloc_example@1.0.1
 - wyatt_authentication_bloc@0.2.1+1
2022-07-10 22:12:37 +02:00
hugo b21aa04557 fix(auth): update form plugin and fix auth 2022-07-10 22:11:16 +02:00
hugo 01df7a0dff fix: ignore google services 2022-07-10 22:10:40 +02:00
hugo 8909274543 feat(form): add helpers 2022-07-10 22:10:12 +02:00
hugo 89f94cf1ff Merge pull request 'form/feature/add_metadata' (#10) from form/feature/add_metadata into master
Reviewed-on: #10
2022-07-10 19:23:54 +00:00
hugo 65774010aa Merge branch 'master' into form/feature/add_metadata 2022-07-10 19:23:40 +00:00
hugo 42810c4fb3 Merge pull request 'chore(deps): update dependency flutter_lints to v2' (#4) from renovate/flutter_lints-2.x into master
Reviewed-on: #4
2022-07-10 19:23:17 +00:00
hugo 39d19b4c7b Merge branch 'master' into renovate/flutter_lints-2.x 2022-07-10 19:23:12 +00:00
hugo 8a0af954bf doc: remove deprecated FormEntry 2022-07-10 21:15:33 +02:00
hugo 94b0ff01ad doc: update readme with usage 2022-07-10 21:13:52 +02:00
hugo 741f05e1be doc: update example 2022-07-10 21:13:43 +02:00
hugo d0f1d07363 feat: add metadata on inputs 2022-07-10 21:13:13 +02:00
hugo 08ac6d8fda feat: update to latest dart version 2022-07-10 21:12:55 +02:00
hugo 9203df94a0 chore(release): publish packages
- wyatt_http_client@1.2.0
2022-06-24 16:01:45 +02:00
hugo a7204f588f Merge pull request 'Feature/middlewares' (#9) from Feature/middlewares into master
Reviewed-on: #9
2022-06-24 14:00:03 +00:00
hugo 81367e5f3a feat(http): add new middleware feature 2022-06-24 15:58:48 +02:00
hugo 14cb3485e4 feat(http): implements doublelinked list for middlewares 2022-06-24 10:47:28 +02:00
hugo 58d3b70bee feat(http): [WIP] implements middleware system 2022-06-23 17:07:04 +02:00
hugo ea4a30ca00 feat(http): [WIP] work on middleware feature 2022-06-23 11:48:32 +02:00
Renovate 83b75ee0e5 chore(deps): update dependency flutter_lints to v2 2022-06-01 14:25:58 +00:00
hugo 9e9ec9ba9b docs(bloc): update license 2022-05-23 21:22:38 +02:00
142 changed files with 7047 additions and 1413 deletions
+1
View File
@@ -196,3 +196,4 @@ $RECYCLE.BIN/
.idea/
*.iml
google-services.json
+156
View File
@@ -3,6 +3,162 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## 2022-08-25
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`wyatt_form_bloc` - `v0.0.4`](#wyatt_form_bloc---v004)
- [`wyatt_authentication_bloc` - `v0.2.1+2`](#wyatt_authentication_bloc---v0212)
Packages with dependency updates only:
> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.
- `wyatt_authentication_bloc` - `v0.2.1+2`
---
#### `wyatt_form_bloc` - `v0.0.4`
- **FEAT**: add form validator class with strategy design pattern.
## 2022-07-18
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`wyatt_type_utils` - `v0.0.2`](#wyatt_type_utils---v002)
---
#### `wyatt_type_utils` - `v0.0.2`
- **FEAT**: add strongly typed Option and Result.
- **FEAT**: add mason generated package.
- **DOCS**: remove useless web example.
## 2022-07-12
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`wyatt_analysis` - `v2.2.1`](#wyatt_analysis---v221)
---
#### `wyatt_analysis` - `v2.2.1`
- **FIX**: import right version.
## 2022-07-11
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`wyatt_analysis` - `v2.2.0`](#wyatt_analysis---v220)
---
#### `wyatt_analysis` - `v2.2.0`
- **FIX**: rename to follow melos versioning and remove redundant rule.
- **FEAT**: promote experimental, update analyzer rules and add some linter rules.
## 2022-07-10
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`form_bloc_example` - `v1.1.0`](#form_bloc_example---v110)
- [`wyatt_form_bloc` - `v0.0.3`](#wyatt_form_bloc---v003)
- [`authentication_bloc_example` - `v1.0.1`](#authentication_bloc_example---v101)
- [`wyatt_authentication_bloc` - `v0.2.1+1`](#wyatt_authentication_bloc---v0211)
Packages with dependency updates only:
> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.
- `authentication_bloc_example` - `v1.0.1`
- `wyatt_authentication_bloc` - `v0.2.1+1`
---
#### `form_bloc_example` - `v1.1.0`
- **FEAT**: add list option validator.
- **DOCS**: add example app.
#### `wyatt_form_bloc` - `v0.0.3`
- **FEAT**: add helpers.
- **FEAT**: add metadata on inputs.
- **FEAT**: update to latest dart version.
## 2022-06-24
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`wyatt_http_client` - `v1.2.0`](#wyatt_http_client---v120)
---
#### `wyatt_http_client` - `v1.2.0`
- **FEAT**: add new middleware feature.
- **FEAT**: implements doublelinked list for middlewares.
- **FEAT**: [WIP] implements middleware system.
- **FEAT**: [WIP] work on middleware feature.
## 2022-05-23
### Changes
+9
View File
@@ -1,3 +1,12 @@
## 2.2.1
- **FIX**: import right version.
## 2.2.0
- **FIX**: rename to follow melos versioning and remove redundant rule.
- **FEAT**: promote experimental, update analyzer rules and add some linter rules.
## 2.1.0
- **FEAT**: add experimental rules in appropriate file.
@@ -0,0 +1,206 @@
# Copyright (C) 2022 WYATT GROUP
# Please see the AUTHORS file for details.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
errors:
todo: ignore
close_sinks: ignore
missing_required_param: error
missing_return: error
exclude:
- example/**
- lib/l10n/**
- lib/generated/**
- lib/generated_plugin_registrant.dart
- test/.test_coverage.dart
linter:
rules:
- always_declare_return_types
- always_require_non_null_named_parameters
- always_put_control_body_on_new_line
- always_put_required_named_parameters_first
- always_use_package_imports
- annotate_overrides
- avoid_bool_literals_in_conditional_expressions
- avoid_catching_errors
- avoid_double_and_int_checks
- avoid_dynamic_calls
- avoid_empty_else
- avoid_equals_and_hash_code_on_mutable_classes
- avoid_escaping_inner_quotes
- avoid_field_initializers_in_const_classes
- avoid_function_literals_in_foreach_calls
- avoid_init_to_null
- avoid_js_rounded_ints
- avoid_null_checks_in_equality_operators
- avoid_positional_boolean_parameters
- avoid_private_typedef_functions
- avoid_redundant_argument_values
- avoid_relative_lib_imports
- avoid_renaming_method_parameters
- avoid_return_types_on_setters
- avoid_returning_null
- avoid_returning_null_for_future
- avoid_returning_null_for_void
- avoid_returning_this
- avoid_setters_without_getters
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- avoid_slow_async_io
- avoid_type_to_string
- avoid_types_as_parameter_names
- avoid_types_on_closure_parameters
- avoid_unused_constructor_parameters
- avoid_void_async
- await_only_futures
- camel_case_extensions
- camel_case_types
- cancel_subscriptions
- cascade_invocations
- cast_nullable_to_non_nullable
- comment_references
- conditional_uri_does_not_exist
- constant_identifier_names
- control_flow_in_finally
- curly_braces_in_flow_control_structures
- depend_on_referenced_packages
- deprecated_consistency
- directives_ordering
- empty_catches
- empty_constructor_bodies
- empty_statements
- eol_at_end_of_file
- exhaustive_cases
- file_names
- flutter_style_todos
- hash_and_equals
- implementation_imports
- iterable_contains_unrelated_type
- join_return_with_assignment
- leading_newlines_in_multiline_strings
- library_names
- library_prefixes
- library_private_types_in_public_api
- lines_longer_than_80_chars
- list_remove_unrelated_type
- literal_only_boolean_expressions
- missing_whitespace_between_adjacent_strings
- no_adjacent_strings_in_list
- no_default_cases
- no_duplicate_case_values
- no_leading_underscores_for_local_identifiers
- no_runtimeType_toString
- non_constant_identifier_names
- noop_primitive_operations
- null_check_on_nullable_type_parameter
- null_closures
- one_member_abstracts
- only_throw_errors
- overridden_fields
- package_api_docs
- package_names
- package_prefixed_library_names
- parameter_assignments
- prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists
- prefer_asserts_with_message
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_constructors_over_static_methods
- prefer_contains
- prefer_equal_for_default_values
- prefer_expression_function_bodies
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_for_elements_to_map_fromIterable
- prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_if_null_operators
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_int_literals
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_iterable_whereType
- prefer_mixin
- prefer_null_aware_operators
- prefer_null_aware_method_calls
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- prefer_void_to_null
- provide_deprecation_message
- recursive_getters
- require_trailing_commas
- slash_for_doc_comments
- sort_unnamed_constructors_first
- test_types_in_equals
- throw_in_finally
- tighten_type_of_initializing_formals
- type_annotate_public_apis
- type_init_formals
- unawaited_futures
- unnecessary_await_in_return
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_constructor_name
- unnecessary_getters_setters
- unnecessary_lambdas
- unnecessary_late
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_checks
- unnecessary_null_in_if_null_operators
- unnecessary_nullable_for_final_variable_declarations
- unnecessary_overrides
- unnecessary_parenthesis
- unnecessary_raw_strings
- unnecessary_statements
- unnecessary_string_escapes
- unnecessary_string_interpolations
- unnecessary_this
- unrelated_type_equality_checks
- unsafe_html
- use_enums
- use_function_type_syntax_for_parameters
- use_if_null_to_convert_nulls_to_bools
- use_is_even_rather_than_modulo
- use_late_for_private_fields_and_variables
- use_named_constants
- use_raw_strings
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_string_buffers
- use_super_parameters
- use_test_throws_matchers
- use_to_and_as_if_applicable
- valid_regexps
- void_checks
@@ -0,0 +1,206 @@
# Copyright (C) 2022 WYATT GROUP
# Please see the AUTHORS file for details.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
errors:
todo: ignore
close_sinks: ignore
missing_required_param: error
missing_return: error
exclude:
- example/**
- lib/l10n/**
- lib/generated/**
- lib/generated_plugin_registrant.dart
- test/.test_coverage.dart
linter:
rules:
- always_declare_return_types
- always_require_non_null_named_parameters
- always_put_control_body_on_new_line
- always_put_required_named_parameters_first
- always_use_package_imports
- annotate_overrides
- avoid_bool_literals_in_conditional_expressions
- avoid_catching_errors
- avoid_double_and_int_checks
- avoid_dynamic_calls
- avoid_empty_else
- avoid_equals_and_hash_code_on_mutable_classes
- avoid_escaping_inner_quotes
- avoid_field_initializers_in_const_classes
- avoid_function_literals_in_foreach_calls
- avoid_init_to_null
- avoid_js_rounded_ints
- avoid_null_checks_in_equality_operators
- avoid_positional_boolean_parameters
- avoid_private_typedef_functions
- avoid_redundant_argument_values
- avoid_relative_lib_imports
- avoid_renaming_method_parameters
- avoid_return_types_on_setters
- avoid_returning_null
- avoid_returning_null_for_future
- avoid_returning_null_for_void
- avoid_returning_this
- avoid_setters_without_getters
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- avoid_slow_async_io
- avoid_type_to_string
- avoid_types_as_parameter_names
- avoid_types_on_closure_parameters
- avoid_unused_constructor_parameters
- avoid_void_async
- await_only_futures
- camel_case_extensions
- camel_case_types
- cancel_subscriptions
- cascade_invocations
- cast_nullable_to_non_nullable
- comment_references
- conditional_uri_does_not_exist
- constant_identifier_names
- control_flow_in_finally
- curly_braces_in_flow_control_structures
- depend_on_referenced_packages
- deprecated_consistency
- directives_ordering
- empty_catches
- empty_constructor_bodies
- empty_statements
- eol_at_end_of_file
- exhaustive_cases
- file_names
- flutter_style_todos
- hash_and_equals
- implementation_imports
- iterable_contains_unrelated_type
- join_return_with_assignment
- leading_newlines_in_multiline_strings
- library_names
- library_prefixes
- library_private_types_in_public_api
- lines_longer_than_80_chars
- list_remove_unrelated_type
- literal_only_boolean_expressions
- missing_whitespace_between_adjacent_strings
- no_adjacent_strings_in_list
- no_default_cases
- no_duplicate_case_values
- no_leading_underscores_for_local_identifiers
- no_runtimeType_toString
- non_constant_identifier_names
- noop_primitive_operations
- null_check_on_nullable_type_parameter
- null_closures
- one_member_abstracts
- only_throw_errors
- overridden_fields
- package_api_docs
- package_names
- package_prefixed_library_names
- parameter_assignments
- prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists
- prefer_asserts_with_message
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_constructors_over_static_methods
- prefer_contains
- prefer_equal_for_default_values
- prefer_expression_function_bodies
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_for_elements_to_map_fromIterable
- prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_if_null_operators
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_int_literals
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_iterable_whereType
- prefer_mixin
- prefer_null_aware_operators
- prefer_null_aware_method_calls
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- prefer_void_to_null
- provide_deprecation_message
- recursive_getters
- require_trailing_commas
- slash_for_doc_comments
- sort_unnamed_constructors_first
- test_types_in_equals
- throw_in_finally
- tighten_type_of_initializing_formals
- type_annotate_public_apis
- type_init_formals
- unawaited_futures
- unnecessary_await_in_return
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_constructor_name
- unnecessary_getters_setters
- unnecessary_lambdas
- unnecessary_late
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_checks
- unnecessary_null_in_if_null_operators
- unnecessary_nullable_for_final_variable_declarations
- unnecessary_overrides
- unnecessary_parenthesis
- unnecessary_raw_strings
- unnecessary_statements
- unnecessary_string_escapes
- unnecessary_string_interpolations
- unnecessary_this
- unrelated_type_equality_checks
- unsafe_html
- use_enums
- use_function_type_syntax_for_parameters
- use_if_null_to_convert_nulls_to_bools
- use_is_even_rather_than_modulo
- use_late_for_private_fields_and_variables
- use_named_constants
- use_raw_strings
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_string_buffers
- use_super_parameters
- use_test_throws_matchers
- use_to_and_as_if_applicable
- valid_regexps
- void_checks
@@ -0,0 +1,32 @@
# Copyright (C) 2022 WYATT GROUP
# Please see the AUTHORS file for details.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
include: package:wyatt_analysis/analysis_options.2.1.1.yaml
linter:
rules:
- avoid_print
- avoid_unnecessary_containers
- avoid_web_libraries_in_flutter
- no_logic_in_create_state
- sized_box_for_whitespace
- sized_box_shrink_expand
- sort_child_properties_last
- use_build_context_synchronously
- use_colored_box
- use_decorated_box
- use_full_hex_values_for_flutter_colors
- use_key_in_widget_constructors
@@ -0,0 +1,32 @@
# Copyright (C) 2022 WYATT GROUP
# Please see the AUTHORS file for details.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
include: package:wyatt_analysis/analysis_options.2.2.1.yaml
linter:
rules:
- avoid_print
- avoid_unnecessary_containers
- avoid_web_libraries_in_flutter
- no_logic_in_create_state
- sized_box_for_whitespace
- sized_box_shrink_expand
- sort_child_properties_last
- use_build_context_synchronously
- use_colored_box
- use_decorated_box
- use_full_hex_values_for_flutter_colors
- use_key_in_widget_constructors
@@ -14,4 +14,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
include: package:wyatt_analysis/analysis_options.flutter.2.0.0.yaml
include: package:wyatt_analysis/analysis_options.flutter.2.2.1.yaml
@@ -14,4 +14,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
include: package:wyatt_analysis/analysis_options.2.0.0.yaml
include: package:wyatt_analysis/analysis_options.2.2.1.yaml
@@ -19,4 +19,4 @@
/// and [Wyatt Studio](https://wyatt-studio.fr).
library wyatt_analysis;
const String wyattAnalysisVersion = '2.0.0';
const String wyattAnalysisVersion = '2.2.1';
+1 -1
View File
@@ -1,7 +1,7 @@
name: wyatt_analysis
description: Lint rules for Dart and Flutter used internally at Wyatt and Wyatt Studio.
repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_analysis
version: 2.1.0
version: 2.2.1
environment:
sdk: '>=2.12.0 <3.0.0'
@@ -1,3 +1,11 @@
## 0.2.1+2
- Update a dependency to the latest release.
## 0.2.1+1
- Update a dependency to the latest release.
## 0.2.1
- **FEAT**: add google, facebook, apple, twitter login.
@@ -0,0 +1,4 @@
## 1.0.1
- Update a dependency to the latest release.
@@ -14,6 +14,14 @@
}
},
"oauth_client": [
{
"client_id": "136771801992-nps7ic22shstvgdv0c2ojvbkj3dkntum.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.example.authentication_bloc_example",
"certificate_hash": "5d8790309e13b68c35e5d4d8437c35c6d15e6131"
}
},
{
"client_id": "136771801992-ncuib3rbu7p4ro4eo5su4vaudn2u4qrv.apps.googleusercontent.com",
"client_type": 3
@@ -77,58 +85,6 @@
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:136771801992:android:d20e0361057e815197203d",
"android_client_info": {
"package_name": "com.example.example"
}
},
"oauth_client": [
{
"client_id": "136771801992-n2pq8oqutvrqj58e05hbavvc7n1jdfjb.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.example.example",
"certificate_hash": "5d8790309e13b68c35e5d4d8437c35c6d15e6131"
}
},
{
"client_id": "136771801992-pj9s88ao7d99pnp4ace75nj1abtmoq7e.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.example.example",
"certificate_hash": "bc8faf31c0b0085dc358b2319684e10c93f42719"
}
},
{
"client_id": "136771801992-ncuib3rbu7p4ro4eo5su4vaudn2u4qrv.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAYS14uXupkS158Q5QAFP1864UrUN_yDSk"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "136771801992-ncuib3rbu7p4ro4eo5su4vaudn2u4qrv.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "136771801992-e585bm1n9b3lv89t4phrl9u0glsg52ua.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.example.example"
}
}
]
}
}
}
],
"configuration_version": "1"
@@ -31,23 +31,29 @@ class App extends StatelessWidget {
static FormData getNormalFormData() {
return const FormData([
FormEntry(formFieldName, Name.pure()),
FormEntry(formFieldPhone, Phone.pure()),
FormEntry(formFieldPro, Boolean.pure()),
FormEntry(formFieldConfirmedPassword, ConfirmedPassword.pure(),
export: false),
FormInput(formFieldName, Name.pure()),
FormInput(formFieldPhone, Phone.pure()),
FormInput(formFieldPro, Boolean.pure()),
FormInput(
formFieldConfirmedPassword,
ConfirmedPassword.pure(),
metadata: FormInputMetadata(export: false),
),
]);
}
static FormData getProFormData() {
return const FormData([
FormEntry(formFieldName, Name.pure()),
FormEntry(formFieldPhone, Phone.pure()),
FormEntry(formFieldPro, Boolean.pure()),
FormEntry(formFieldSiren, Siren.pure()),
FormEntry(formFieldIban, Iban.pure()),
FormEntry(formFieldConfirmedPassword, ConfirmedPassword.pure(),
export: false),
FormInput(formFieldName, Name.pure()),
FormInput(formFieldPhone, Phone.pure()),
FormInput(formFieldPro, Boolean.pure()),
FormInput(formFieldSiren, Siren.pure()),
FormInput(formFieldIban, Iban.pure()),
FormInput(
formFieldConfirmedPassword,
ConfirmedPassword.pure(),
metadata: FormInputMetadata(export: false),
),
]);
}
@@ -103,42 +109,42 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
AuthenticationRepositoryInterface _authenticationRepository =
AuthenticationRepositoryInterface authenticationRepository =
AuthenticationRepositoryFirebase();
AuthenticationCubit _authenticationCubit = AuthenticationCubit(
authenticationRepository: _authenticationRepository,
AuthenticationCubit authenticationCubit = AuthenticationCubit(
authenticationRepository: authenticationRepository,
onAuthSuccess: onAuthSuccess,
);
SignUpCubit _signUpCubit = SignUpCubit(
authenticationRepository: _authenticationRepository,
authenticationCubit: _authenticationCubit,
SignUpCubit signUpCubit = SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: getNormalFormData(),
onSignUpSuccess: onSignUpSuccess,
);
SignInCubit _signInCubit = SignInCubit(
authenticationRepository: _authenticationRepository,
authenticationCubit: _authenticationCubit,
SignInCubit signInCubit = SignInCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
);
return MultiRepositoryProvider(
providers: [
RepositoryProvider<AuthenticationRepositoryInterface>(
create: (context) => _authenticationRepository,
create: (context) => authenticationRepository,
),
],
child: MultiBlocProvider(
providers: [
BlocProvider<AuthenticationCubit>(
create: (context) => _authenticationCubit..init(),
create: (context) => authenticationCubit..init(),
),
BlocProvider<SignUpCubit>(
create: (context) => _signUpCubit,
create: (context) => signUpCubit,
),
BlocProvider<SignInCubit>(
create: (context) => _signInCubit,
create: (context) => signInCubit,
),
],
child: const AppView(),
@@ -37,7 +37,7 @@ class _NameInput extends StatelessWidget {
labelText: 'name',
helperText: '',
errorText:
state.data.input(formFieldName).invalid ? 'invalid name' : null,
state.data.isNotValid(formFieldName) ? 'invalid name' : null,
),
);
},
@@ -58,7 +58,7 @@ class _PhoneInput extends StatelessWidget {
decoration: InputDecoration(
labelText: 'phone',
helperText: '',
errorText: state.data.input(formFieldPhone).invalid
errorText: state.data.isNotValid(formFieldPhone)
? 'invalid phone'
: null,
),
@@ -81,7 +81,7 @@ class _SirenInput extends StatelessWidget {
decoration: InputDecoration(
labelText: 'siren',
helperText: '',
errorText: state.data.input(formFieldSiren).invalid
errorText: state.data.isNotValid(formFieldSiren)
? 'invalid SIREN'
: null,
),
@@ -105,7 +105,7 @@ class _IbanInput extends StatelessWidget {
labelText: 'iban',
helperText: '',
errorText:
state.data.input(formFieldIban).invalid ? 'invalid IBAN' : null,
state.data.isNotValid(formFieldIban) ? 'invalid IBAN' : null,
),
);
},
@@ -149,8 +149,7 @@ class _PasswordInput extends StatelessWidget {
.read<SignUpCubit>()
.state
.data
.input(formFieldConfirmedPassword)
.value,
.valueOf<String, ValidationError>(formFieldConfirmedPassword),
),
);
},
@@ -185,7 +184,7 @@ class _ConfirmPasswordInput extends StatelessWidget {
decoration: InputDecoration(
labelText: 'confirm password',
helperText: '',
errorText: state.data.input(formFieldConfirmedPassword).invalid
errorText: state.data.isNotValid(formFieldConfirmedPassword)
? 'passwords do not match'
: null,
),
@@ -203,7 +202,7 @@ class _CheckIsProInput extends StatelessWidget {
trailing: BlocBuilder<SignUpCubit, SignUpState>(
builder: (context, state) {
return Checkbox(
value: state.data.input<bool>(formFieldPro).value,
value: state.data.valueOf<bool, ValidationError>(formFieldPro),
onChanged: (isPro) {
final value =
isPro!; // tristate is false, so value can't be null
@@ -297,7 +296,7 @@ class SignUpForm extends StatelessWidget {
const SizedBox(height: 8),
BlocBuilder<SignUpCubit, SignUpState>(
builder: (context, state) {
if (state.data.input<bool>(formFieldPro).value) {
if (state.data.valueOf<bool, ValidationError>(formFieldPro)) {
return Column(children: [
_SirenInput(),
const SizedBox(height: 8),
@@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
version: 1.0.1
environment:
sdk: ">=2.16.2 <3.0.0"
@@ -38,7 +38,7 @@ dependencies:
wyatt_form_bloc:
git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_form_bloc-v0.0.2
ref: wyatt_form_bloc-v0.0.4
path: packages/wyatt_form_bloc
# The following adds the Cupertino Icons font to your application.
@@ -55,7 +55,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^1.0.0
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -0,0 +1,100 @@
name: example_router
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
environment:
sdk: ">=2.17.6 <3.0.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
go_router: ^4.2.8
firebase_core: ^1.21.0
flutter_bloc: ^8.1.1
wyatt_authentication_bloc:
path: "../"
wyatt_form_bloc:
git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_form_bloc-v0.0.4
path: packages/wyatt_form_bloc
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
@@ -33,7 +33,7 @@ class PasswordResetCubit extends Cubit<PasswordResetState> {
emit(
state.copyWith(
email: email,
status: FormData.validate([email]),
status: FormStatus.validate([email]),
),
);
}
@@ -39,7 +39,7 @@ class SignInCubit extends Cubit<SignInState> {
emit(
state.copyWith(
email: email,
status: FormData.validate([email, state.password]),
status: FormStatus.validate([email, state.password]),
),
);
}
@@ -49,7 +49,7 @@ class SignInCubit extends Cubit<SignInState> {
emit(
state.copyWith(
password: password,
status: FormData.validate([state.email, password]),
status: FormStatus.validate([state.email, password]),
),
);
}
@@ -44,16 +44,16 @@ class SignUpCubit extends Cubit<SignUpState> {
void emailChanged(String value) {
final Email email = Email.dirty(value);
final List<FormInput<dynamic, ValidationError>> inputsToValidate = [
final List<FormInputValidator<dynamic, ValidationError>> toValidate = [
email,
state.password,
...state.data.inputs<dynamic>(),
...state.data.validators<dynamic, ValidationError>(),
];
emit(
state.copyWith(
email: email,
status: FormData.validate(inputsToValidate),
status: FormStatus.validate(toValidate),
),
);
}
@@ -61,26 +61,26 @@ class SignUpCubit extends Cubit<SignUpState> {
void passwordChanged(String value) {
final Password password = Password.dirty(value);
final List<FormInput<dynamic, ValidationError>> inputsToValidate = [
final List<FormInputValidator<dynamic, ValidationError>> toValidate = [
state.email,
password,
...state.data.inputs<dynamic>(),
...state.data.validators<dynamic, ValidationError>(),
];
emit(
state.copyWith(
password: password,
status: FormData.validate(inputsToValidate),
status: FormStatus.validate(toValidate),
),
);
}
// Take from wyatt_form_bloc/wyatt_form_bloc.dart
void dataChanged<T>(String field, FormInput dirtyValue) {
void dataChanged<T>(String field, FormInputValidator dirtyValue) {
final _form = state.data.clone();
if (_form.contains(field)) {
_form.update(field, dirtyValue);
_form.updateValidator(field, dirtyValue);
} else {
throw Exception('Form field $field not found');
}
@@ -88,8 +88,12 @@ class SignUpCubit extends Cubit<SignUpState> {
emit(
state.copyWith(
data: _form,
status: FormData.validate(
[state.email, state.password, ..._form.inputs<dynamic>()],
status: FormStatus.validate(
[
state.email,
state.password,
...state.data.validators<dynamic, ValidationError>(),
],
),
),
);
@@ -120,8 +124,12 @@ class SignUpCubit extends Cubit<SignUpState> {
emit(
state.copyWith(
data: _form,
status: FormData.validate(
[state.email, state.password, ..._form.inputs<dynamic>()],
status: FormStatus.validate(
[
state.email,
state.password,
...state.data.validators<dynamic, ValidationError>(),
],
),
),
);
@@ -1,7 +1,7 @@
name: wyatt_authentication_bloc
description: Authentication BLoC for Flutter
repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_authentication_bloc
version: 0.2.1
version: 0.2.1+2
environment:
sdk: ">=2.15.1 <3.0.0"
@@ -23,7 +23,7 @@ dependencies:
wyatt_form_bloc:
git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_form_bloc-v0.0.2
ref: wyatt_form_bloc-v0.0.4
path: packages/wyatt_form_bloc
@@ -35,5 +35,5 @@ dev_dependencies:
wyatt_analysis:
git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_analysis-v2.1.0
ref: wyatt_analysis-v2.2.1
path: packages/wyatt_analysis
@@ -68,9 +68,9 @@ void main() {
SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
).state,
SignUpState(data: FormData.empty()),
const SignUpState(data: FormData.empty()),
);
});
@@ -80,11 +80,11 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
act: (SignUpCubit cubit) => cubit.emailChanged(invalidEmailString),
expect: () => <SignUpState>[
SignUpState(
const SignUpState(
email: invalidEmail,
status: FormStatus.invalid,
data: FormData.empty(),
@@ -97,15 +97,15 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
seed: () => SignUpState(
seed: () => const SignUpState(
password: validPassword,
data: FormData.empty(),
),
act: (SignUpCubit cubit) => cubit.emailChanged(validEmailString),
expect: () => <SignUpState>[
SignUpState(
const SignUpState(
email: validEmail,
password: validPassword,
status: FormStatus.valid,
@@ -121,12 +121,12 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
act: (SignUpCubit cubit) =>
cubit.passwordChanged(invalidPasswordString),
expect: () => <SignUpState>[
SignUpState(
const SignUpState(
password: invalidPassword,
status: FormStatus.invalid,
data: FormData.empty(),
@@ -139,15 +139,15 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
seed: () => SignUpState(
seed: () => const SignUpState(
email: validEmail,
data: FormData.empty(),
),
act: (SignUpCubit cubit) => cubit.passwordChanged(validPasswordString),
expect: () => <SignUpState>[
SignUpState(
const SignUpState(
email: validEmail,
password: validPassword,
status: FormStatus.valid,
@@ -163,7 +163,7 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
act: (SignUpCubit cubit) => cubit.signUpFormSubmitted(),
expect: () => const <SignUpState>[],
@@ -174,9 +174,9 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
seed: () => SignUpState(
seed: () => const SignUpState(
status: FormStatus.valid,
email: validEmail,
password: validPassword,
@@ -199,9 +199,9 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
seed: () => SignUpState(
seed: () => const SignUpState(
status: FormStatus.valid,
email: validEmail,
password: validPassword,
@@ -209,13 +209,13 @@ void main() {
),
act: (SignUpCubit cubit) => cubit.signUpFormSubmitted(),
expect: () => <SignUpState>[
SignUpState(
const SignUpState(
status: FormStatus.submissionInProgress,
email: validEmail,
password: validPassword,
data: FormData.empty(),
),
SignUpState(
const SignUpState(
status: FormStatus.submissionSuccess,
email: validEmail,
password: validPassword,
@@ -238,9 +238,9 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
seed: () => SignUpState(
seed: () => const SignUpState(
status: FormStatus.valid,
email: validEmail,
password: validPassword,
@@ -248,13 +248,13 @@ void main() {
),
act: (SignUpCubit cubit) => cubit.signUpFormSubmitted(),
expect: () => <SignUpState>[
SignUpState(
const SignUpState(
status: FormStatus.submissionInProgress,
email: validEmail,
password: validPassword,
data: FormData.empty(),
),
SignUpState(
const SignUpState(
status: FormStatus.submissionFailure,
email: validEmail,
password: validPassword,
@@ -26,10 +26,10 @@ void main() {
group('SignUpState', () {
test('supports value comparisons', () {
expect(
SignUpState(
const SignUpState(
data: FormData.empty(),
),
SignUpState(
const SignUpState(
data: FormData.empty(),
),
);
@@ -37,10 +37,10 @@ void main() {
test('returns same object when no properties are passed', () {
expect(
SignUpState(
const SignUpState(
data: FormData.empty(),
).copyWith(),
SignUpState(
const SignUpState(
data: FormData.empty(),
),
);
@@ -48,10 +48,10 @@ void main() {
test('returns object with updated status when status is passed', () {
expect(
SignUpState(
const SignUpState(
data: FormData.empty(),
).copyWith(status: FormStatus.pure),
SignUpState(
const SignUpState(
data: FormData.empty(),
),
);
@@ -59,10 +59,10 @@ void main() {
test('returns object with updated email when email is passed', () {
expect(
SignUpState(
const SignUpState(
data: FormData.empty(),
).copyWith(email: email),
SignUpState(
const SignUpState(
email: email,
data: FormData.empty(),
),
@@ -71,10 +71,10 @@ void main() {
test('returns object with updated password when password is passed', () {
expect(
SignUpState(
const SignUpState(
data: FormData.empty(),
).copyWith(password: password),
SignUpState(
const SignUpState(
password: password,
data: FormData.empty(),
),
@@ -85,12 +85,12 @@ void main() {
'returns object with updated data'
' when data is passed', () {
expect(
SignUpState(
const SignUpState(
data: FormData.empty(),
).copyWith(
data: const FormData(
[
FormEntry(
FormInput(
'field',
Name.pure(),
),
@@ -100,7 +100,7 @@ void main() {
const SignUpState(
data: FormData(
[
FormEntry(
FormInput(
'field',
Name.pure(),
),
-11
View File
@@ -1,14 +1,3 @@
/*
* Author: Hugo Pointcheval
* Email: git@pcl.ovh
* -----
* File: LICENSE
* Created Date: 20/05/2022 16:47:39
* Last Modified: 20/05/2022 16:47:39
* -----
* Copyright (c) 2022
*/
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
+13
View File
@@ -116,6 +116,19 @@ Widget onBuild(BuildContext context, CounterState state) {
}
```
If needed, you can wrap what depends on the state with the function `onWrap`.
```dart
@override
Widget onWrap(BuildContext context, Widget child) {
return Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: child,
);
```
> Note: you can override `onBuild`, but also `onListen`, `shouldBuildWhen` and `shouldListenWhen` methods.
### Both BlocProvider and BlocConsumer
@@ -36,17 +36,20 @@ abstract class BlocBaseConsumerScreen<B extends BlocBase<S>, S extends Object>
/// must return a widget.
Widget onBuild(BuildContext context, S state);
/// The [onWrap] function which will be invoked on each widget build.
/// The [onWrap] takes the `BuildContext`
/// Used to wrap which depends on the state.
Widget onWrap(BuildContext context, Widget child) => child;
/// Takes the `BuildContext` along with the `state`
/// and is responsible for executing in response to `state` changes.
void onListen(BuildContext context, S state) {}
@override
Widget build(BuildContext context) {
return BlocConsumer<B, S>(
listenWhen: shouldListenWhen,
listener: onListen,
buildWhen: shouldBuildWhen,
builder: onBuild,
);
}
Widget build(BuildContext context) => BlocConsumer<B, S>(
listenWhen: shouldListenWhen,
listener: onListen,
buildWhen: shouldBuildWhen,
builder: (context, state) => onWrap(context, onBuild(context, state)),
);
}
@@ -28,10 +28,8 @@ abstract class BlocBaseProviderScreen<B extends BlocBase<S>, S extends Object>
Widget buildChild(BuildContext context);
@override
Widget build(BuildContext context) {
return BlocProvider<B>(
create: (_) => create(context),
child: Builder(builder: buildChild),
);
}
Widget build(BuildContext context) => BlocProvider<B>(
create: (_) => create(context),
child: Builder(builder: buildChild),
);
}
@@ -26,10 +26,8 @@ abstract class BlocBaseScreen<B extends BlocBase<S>, S extends Object>
B create(BuildContext context);
@override
Widget build(BuildContext context) {
return BlocProvider<B>(
create: (_) => create(context),
child: super.build(context),
);
}
Widget build(BuildContext context) => BlocProvider<B>(
create: (_) => create(context),
child: super.build(context),
);
}
@@ -21,8 +21,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
/// [Bloc] and [Cubit] widgets.
mixin BlocBaseProviderMixin<B extends BlocBase<Object>> {
/// Returns the [BlocBase] used by this [BlocBaseProviderMixin].
B bloc(BuildContext context) => BlocProvider.of<B>(context);
B bloc(BuildContext context) => context.read<B>();
/// Returns the [BlocBase] used by this [BlocBaseProviderMixin].
R repo<R>(BuildContext context) => RepositoryProvider.of<R>(context);
R repo<R>(BuildContext context) => context.read<R>();
}
@@ -20,7 +20,5 @@ import 'package:flutter_bloc/flutter_bloc.dart';
/// [Bloc] specific mixin that provides implementation
/// of helper methods for events.
mixin BlocProviderMixin<B extends Bloc<E, Object>, E> {
void add(BuildContext context, E event) {
BlocProvider.of<B>(context).add(event);
}
void add(BuildContext context, E event) => context.read<B>().add(event);
}
+1 -1
View File
@@ -21,5 +21,5 @@ dev_dependencies:
wyatt_analysis:
git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_analysis-v2.1.0
ref: wyatt_analysis-v2.2.1
path: packages/wyatt_analysis
+1 -1
View File
@@ -10,6 +10,6 @@ dev_dependencies:
wyatt_analysis:
git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_analysis-v2.1.0
ref: wyatt_analysis-v2.2.1
path: packages/wyatt_analysis
dependencies: {meta: ^1.8.0}
@@ -14,6 +14,14 @@
}
},
"oauth_client": [
{
"client_id": "136771801992-nps7ic22shstvgdv0c2ojvbkj3dkntum.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.example.authentication_bloc_example",
"certificate_hash": "5d8790309e13b68c35e5d4d8437c35c6d15e6131"
}
},
{
"client_id": "136771801992-ncuib3rbu7p4ro4eo5su4vaudn2u4qrv.apps.googleusercontent.com",
"client_type": 3
@@ -77,58 +85,6 @@
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:136771801992:android:d20e0361057e815197203d",
"android_client_info": {
"package_name": "com.example.example"
}
},
"oauth_client": [
{
"client_id": "136771801992-n2pq8oqutvrqj58e05hbavvc7n1jdfjb.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.example.example",
"certificate_hash": "5d8790309e13b68c35e5d4d8437c35c6d15e6131"
}
},
{
"client_id": "136771801992-pj9s88ao7d99pnp4ace75nj1abtmoq7e.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.example.example",
"certificate_hash": "bc8faf31c0b0085dc358b2319684e10c93f42719"
}
},
{
"client_id": "136771801992-ncuib3rbu7p4ro4eo5su4vaudn2u4qrv.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAYS14uXupkS158Q5QAFP1864UrUN_yDSk"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "136771801992-ncuib3rbu7p4ro4eo5su4vaudn2u4qrv.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "136771801992-e585bm1n9b3lv89t4phrl9u0glsg52ua.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.example.example"
}
}
]
}
}
}
],
"configuration_version": "1"
@@ -49,7 +49,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^1.0.0
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
+1 -1
View File
@@ -23,5 +23,5 @@ dev_dependencies:
wyatt_analysis:
git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_analysis-v2.1.0
ref: wyatt_analysis-v2.2.1
path: packages/wyatt_analysis
+10
View File
@@ -1,3 +1,13 @@
## 0.0.4
- **FEAT**: add form validator class with strategy design pattern.
## 0.0.3
- **FEAT**: add helpers.
- **FEAT**: add metadata on inputs.
- **FEAT**: update to latest dart version.
## 0.0.2
- **FEAT**: add list option validator.
+126 -9
View File
@@ -30,21 +30,22 @@ Form Bloc for Dart & Flutter.
## Features
- Form
* FormInput: *atom of a form*
* FormInputValidator:
- Store data
- Validate this data
* FormEntry: *shell of this atom*
- Associate a key to an input
- Configure form attribute (exportation, name...)
* FormData: *collection of entries*
- Contain all entries
* FormInputMetadata:
- Store infos and options about an input.
* FormInput:
- Associaite a key, to a validator and metadata.
* FormData: *collection of inputs*
- Contain all inputs
- Basic set operation
- FormDataCubit
* Data management behind a form.
- Use entries to pass a FormData object
- You can use several pre configured FormInput for validation
- You can use updateFormData() to change FormData and validators during runtime (intersection, union, difference or replace)
- You can use several pre configured `FormInputValidator` for validation
- You can use updateFormData() to change `FormData` during runtime (intersection, union, difference or replace)
- Consistent
* Every class have same naming convention
@@ -59,4 +60,120 @@ import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
## Usage
todo
Firstly, you have to create your form inputs.
```dart
static List<FormInput> getNormalEntries() {
return const [
FormInput(formFieldName, Name.pure()),
FormInput(formFieldEmail, Email.pure()),
FormInput(formFieldPhone, Phone.pure()),
FormInput(formFieldList, ListOption<String>.pure(defaultValue: ['checkbox3'])),
FormInput(formFieldRadio, TextString.pure()),
FormInput(formFieldPro, Boolean.pure(), metadata: FormInputMetadata<void>(name: 'business')),
FormInput(formFieldHidden, Boolean.pure(), metadata: FormInputMetadata<void>(export: false)),
];
}
static List<FormInput> getBusinessEntries() {
const entries = [
FormInput(formFieldSiren, Siren.pure()),
FormInput(formFieldIban, Iban.pure()),
];
return getNormalEntries() + entries;
}
static FormData getNormalFormData() {
return FormData(getNormalEntries());
}
static FormData getProFormData() {
return FormData(getBusinessEntries());
}
```
> Let's create functions to enable dynamic changes in runtime.
> When creating metadata with `FormInputMetadata<T>`, `T` is the type of `extra` object.
Then a `FormData` is a collection of inputs. You can `validate`, `contains`, `intersection`, `difference`, `union`, or `clone` this data.
After that, you have to extends `FormDataCubit`.
```dart
class CustomFormCubit extends FormDataCubit {
CustomFormCubit({required FormData inputs}) : super(inputs: inputs);
@override
Future<void> submitForm() {
log(state.data.toMap().toString());
// Handle all your logic here.
emit(...)
}
}
```
> The submited form is in `state.data` !
Don't forget to create and provide it !
```dart
FormDataCubit _formCubit = CustomFormCubit(inputs: getNormalFormData());
return BlocProvider(
create: (context) => _formCubit,
child: const WidgetTree(),
);
```
You can now create the first form input !
```dart
class _EmailInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) {
return TextField(
onChanged: (email) => context
.read<FormDataCubit>()
.dataChanged(formFieldEmail, Email.dirty(email)),
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'email',
helperText: '',
errorText: state.data.isNotValid(formFieldEmail)
? 'invalid email'
: null,
),
);
},
);
}
}
```
Then, you can create the trigger
```dart
class _SignUpButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>(
buildWhen: (previous, current) => previous.status != current.status,
builder: (context, state) {
return state.status.isSubmissionInProgress
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: state.status.isValidated
? () => context.read<FormDataCubit>().submitForm()
: null,
child: const Text('SIGN UP'),
);
},
);
}
}
```
> With `state.status` you can access the status of the form to check if there is an error or if it's in submission.
@@ -1 +1,4 @@
include: package:wyatt_analysis/analysis_options.yaml
analyzer:
exclude: "!example/**"
+22 -2
View File
@@ -1,10 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
# This file should be version controlled.
version:
revision: 5464c5bac742001448fe4fc0597be939379f88ea
revision: 85684f9300908116a78138ea4c6036c35c9a1236
channel: stable
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- platform: web
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
@@ -0,0 +1,5 @@
## 1.1.0
- **FEAT**: add list option validator.
- **DOCS**: add example app.
@@ -16,31 +16,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:form_bloc_example/app/metadata.dart';
import 'package:form_bloc_example/constants.dart';
import 'package:form_bloc_example/cubit/custom_form_cubit.dart';
import 'package:form_bloc_example/sign_up/sign_up_page.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
const blue = Metadata(category: Category.perso);
const red = Metadata(category: Category.business);
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
static List<FormEntry> getNormalEntries() {
static List<FormInput> getNormalEntries() {
return const [
FormEntry(formFieldName, Name.pure()),
FormEntry(formFieldEmail, Email.pure()),
FormEntry(formFieldPhone, Phone.pure()),
FormEntry(
FormInput(formFieldName, Name.pure(), metadata: FormInputMetadata<Metadata>(extra: blue)),
FormInput(formFieldEmail, Email.pure(), metadata: FormInputMetadata<Metadata>(extra: blue)),
FormInput(formFieldPhone, Phone.pure(), metadata: FormInputMetadata<Metadata>(extra: red)),
FormInput(
formFieldList, ListOption<String>.pure(defaultValue: ['checkbox3'])),
FormEntry(formFieldRadio, TextString.pure()),
FormEntry(formFieldPro, Boolean.pure(), name: 'business'),
FormEntry(formFieldHidden, Boolean.pure(), export: false),
FormInput(formFieldRadio, TextString.pure()),
FormInput(formFieldPro, Boolean.pure(),
metadata: FormInputMetadata<Metadata>(name: 'business')),
FormInput(formFieldHidden, Boolean.pure(),
metadata: FormInputMetadata<Metadata>(export: false, extra: blue)),
];
}
static List<FormEntry> getBusinessEntries() {
static List<FormInput> getBusinessEntries() {
const entries = [
FormEntry(formFieldSiren, Siren.pure()),
FormEntry(formFieldIban, Iban.pure()),
FormInput(formFieldSiren, Siren.pure()),
FormInput(formFieldIban, Iban.pure()),
];
return getNormalEntries() + entries;
}
@@ -55,10 +61,10 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
FormDataCubit _formCubit = CustomFormCubit(entries: getNormalFormData());
FormDataCubit formCubit = CustomFormCubit(inputs: getNormalFormData());
return BlocProvider(
create: (context) => _formCubit,
create: (context) => formCubit,
child: const WidgetTree(),
);
}
@@ -1,3 +1,4 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
@@ -14,12 +15,20 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
bool listEquals<T>(List<T>? a, List<T>? b) {
if (a == null) return b == null;
if (b == null || a.length != b.length) return false;
if (identical(a, b)) return true;
for (int index = 0; index < a.length; index += 1) {
if (a[index] != b[index]) return false;
enum Category {
perso,
business
}
class Metadata {
final Category category;
const Metadata({
required this.category,
});
@override
String toString() {
return category.toString();
}
return true;
}
@@ -16,13 +16,10 @@
import 'dart:developer';
import 'package:meta/meta.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
part 'custom_form_state.dart';
class CustomFormCubit extends FormDataCubit {
CustomFormCubit({required FormData entries}) : super(entries: entries);
CustomFormCubit({required FormData inputs}) : super(inputs: inputs);
@override
Future<void> submitForm() {
@@ -19,35 +19,83 @@ import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:form_bloc_example/app/app.dart';
import 'package:form_bloc_example/app/metadata.dart';
import 'package:form_bloc_example/constants.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
class CategoryIndicator extends StatelessWidget {
const CategoryIndicator(
{Key? key, required this.field, required this.builder})
: super(key: key);
final String field;
final Function(BuildContext context, FormDataState state) builder;
Color computeColor(Metadata meta) {
switch (meta.category) {
case Category.perso:
return Colors.blue;
case Category.business:
return Colors.red;
}
}
@override
Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>(
builder: ((context, state) {
final meta = state.data.metadataOf(field).extra as Metadata;
final color = computeColor(meta);
return Row(
children: [
Container(
height: 8,
width: 8,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(8),
),
),
const SizedBox(width: 20,),
SizedBox(
width: MediaQuery.of(context).size.width - 45,
child: builder(context, state),
)
],
);
}),
);
}
}
class _NameInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) {
return TextField(
onChanged: (name) => context
.read<FormDataCubit>()
.dataChanged(formFieldName, Name.dirty(name)),
keyboardType: TextInputType.name,
decoration: InputDecoration(
labelText: 'name',
helperText: '',
errorText:
state.data.input(formFieldName).invalid ? 'invalid name' : null,
),
);
},
);
return CategoryIndicator(
field: formFieldName,
builder: (context, state) {
return TextField(
onChanged: (name) => context
.read<FormDataCubit>()
.dataChanged(formFieldName, Name.dirty(name)),
keyboardType: TextInputType.name,
decoration: InputDecoration(
labelText: 'name',
helperText: '',
errorText:
state.data.isNotValid(formFieldName) ? 'invalid name' : null,
),
);
});
}
}
class _EmailInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>(
return CategoryIndicator(
field: formFieldEmail,
builder: (context, state) {
return TextField(
onChanged: (email) => context
@@ -57,9 +105,8 @@ class _EmailInput extends StatelessWidget {
decoration: InputDecoration(
labelText: 'email',
helperText: '',
errorText: state.data.input(formFieldEmail).invalid
? 'invalid email'
: null,
errorText:
state.data.isNotValid(formFieldEmail) ? 'invalid email' : null,
),
);
},
@@ -70,7 +117,8 @@ class _EmailInput extends StatelessWidget {
class _PhoneInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>(
return CategoryIndicator(
field: formFieldPhone,
builder: (context, state) {
return TextField(
onChanged: (phone) => context
@@ -80,9 +128,8 @@ class _PhoneInput extends StatelessWidget {
decoration: InputDecoration(
labelText: 'phone',
helperText: '',
errorText: state.data.input(formFieldPhone).invalid
? 'invalid phone'
: null,
errorText:
state.data.isNotValid(formFieldPhone) ? 'invalid phone' : null,
),
);
},
@@ -93,7 +140,8 @@ class _PhoneInput extends StatelessWidget {
class _SirenInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>(
return CategoryIndicator(
field: formFieldName,
builder: (context, state) {
return TextField(
onChanged: (siren) => context
@@ -103,9 +151,8 @@ class _SirenInput extends StatelessWidget {
decoration: InputDecoration(
labelText: 'siren',
helperText: '',
errorText: state.data.input(formFieldSiren).invalid
? 'invalid SIREN'
: null,
errorText:
state.data.isNotValid(formFieldSiren) ? 'invalid SIREN' : null,
),
);
},
@@ -127,7 +174,7 @@ class _IbanInput extends StatelessWidget {
labelText: 'iban',
helperText: '',
errorText:
state.data.input(formFieldIban).invalid ? 'invalid IBAN' : null,
state.data.isNotValid(formFieldIban) ? 'invalid IBAN' : null,
),
);
},
@@ -140,9 +187,9 @@ class _CheckListInput extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) {
final _input =
state.data.input<List<String>>(formFieldList) as ListOption<String>;
final _options = _input.value;
final input =
state.data.validatorOf<ListOption<String>>(formFieldList);
final options = input.value;
return Column(
mainAxisSize: MainAxisSize.min,
@@ -150,33 +197,33 @@ class _CheckListInput extends StatelessWidget {
ListTile(
title: const Text('Checkbox1'),
trailing: Checkbox(
value: _options.contains('checkbox1'),
value: options.contains('checkbox1'),
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldList,
_input.select('checkbox1'),
input.select('checkbox1'),
);
}),
),
ListTile(
title: const Text('Checkbox2'),
trailing: Checkbox(
value: _options.contains('checkbox2'),
value: options.contains('checkbox2'),
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldList,
_input.select('checkbox2'),
input.select('checkbox2'),
);
}),
),
ListTile(
title: const Text('Checkbox3 (default)'),
trailing: Checkbox(
value: _options.contains('checkbox3'),
value: options.contains('checkbox3'),
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldList,
_input.select('checkbox3'),
input.select('checkbox3'),
);
}),
),
@@ -192,8 +239,7 @@ class _RadioListInput extends StatelessWidget {
Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) {
final _input =
state.data.input<String>(formFieldRadio) as TextString;
final input = state.data.validatorOf<TextString>(formFieldRadio);
return Column(
mainAxisSize: MainAxisSize.min,
@@ -202,7 +248,7 @@ class _RadioListInput extends StatelessWidget {
title: const Text('Radio1'),
trailing: Radio<bool>(
groupValue: true,
value: _input.value == 'radio1',
value: input.value == 'radio1',
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldRadio,
@@ -214,7 +260,7 @@ class _RadioListInput extends StatelessWidget {
title: const Text('Radio2'),
trailing: Radio<bool>(
groupValue: true,
value: _input.value == 'radio2',
value: input.value == 'radio2',
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldRadio,
@@ -226,7 +272,7 @@ class _RadioListInput extends StatelessWidget {
title: const Text('Radio3'),
trailing: Radio<bool>(
groupValue: true,
value: _input.value == 'radio3',
value: input.value == 'radio3',
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldRadio,
@@ -249,7 +295,7 @@ class _CheckHiddenInput extends StatelessWidget {
trailing: BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) {
return Checkbox(
value: state.data.input<bool>(formFieldHidden).value,
value: state.data.validatorOf<Boolean>(formFieldHidden).value,
onChanged: (v) {
final value = v!; // state is false, so value can't be null
@@ -272,7 +318,7 @@ class _CheckIsProInput extends StatelessWidget {
trailing: BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) {
return Checkbox(
value: state.data.input<bool>(formFieldPro).value,
value: state.data.validatorOf<Boolean>(formFieldPro).value,
onChanged: (isPro) {
final value = isPro!; // state is false, so value can't be null
@@ -332,6 +378,20 @@ class _DebugButton extends StatelessWidget {
}
}
class _ResetButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) {
return ElevatedButton(
onPressed: () => context.read<FormDataCubit>().resetForm(),
child: const Text('RESET'),
);
},
);
}
}
class SignUpForm extends StatelessWidget {
const SignUpForm({Key? key}) : super(key: key);
@@ -370,7 +430,7 @@ class SignUpForm extends StatelessWidget {
const SizedBox(height: 8),
BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) {
if (state.data.input<bool>(formFieldPro).value) {
if (state.data.validatorOf<Boolean>(formFieldPro).value) {
return Column(children: [
_SirenInput(),
const SizedBox(height: 8),
@@ -385,6 +445,8 @@ class SignUpForm extends StatelessWidget {
_SignUpButton(),
const SizedBox(height: 8),
_DebugButton(),
const SizedBox(height: 8),
_ResetButton(),
],
),
),
@@ -15,10 +15,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
version: 1.1.0
environment:
sdk: ">=2.16.2 <3.0.0"
sdk: ">=2.17.2 <3.0.0"
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
@@ -47,7 +47,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^1.0.0
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="example">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>example</title>
<link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
var serviceWorkerVersion = null;
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head>
<body>
<script>
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
}
}).then(function(engineInitializer) {
return engineInitializer.initializeEngine();
}).then(function(appRunner) {
return appRunner.runApp();
});
});
</script>
</body>
</html>
@@ -0,0 +1,35 @@
{
"name": "example",
"short_name": "example",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
@@ -17,38 +17,55 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';
import 'package:wyatt_form_bloc/src/enums/enums.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
import 'package:wyatt_form_bloc/src/validators/form/every_input_validator.dart';
part 'form_data_state.dart';
abstract class FormDataCubit extends Cubit<FormDataState> {
FormDataCubit({required FormData entries})
: super(FormDataState(data: entries));
FormValidator validationStrategy;
FormData formCopy;
FormDataCubit({
required FormData inputs,
FormValidator validator = const EveryInputValidator(),
}) : formCopy = inputs.clone(),
validationStrategy = validator,
super(FormDataState(data: inputs));
/// Change value of a field.
///
/// Inputs:
/// - `field`: The key of the field to change.
/// - `dirtyValue`: The new value of the field.
void dataChanged(String field, FormInput dirtyValue) {
final _form = state.data.clone();
/// - `dirtyValue`: The new value of the field. (Wrapped in a dirty validator)
void dataChanged<V>(
String field,
FormInputValidator<V, ValidationError> dirtyValue,
) {
final form = state.data.clone();
if (_form.contains(field)) {
_form.update(field, dirtyValue);
if (form.contains(field)) {
form.updateValidator(field, dirtyValue);
} else {
throw Exception('Form field $field not found');
}
emit(
state.copyWith(
data: _form,
status: _form.selfValidate(),
data: form,
status: validationStrategy.validate(form),
),
);
}
/// Just validate the form manually. (Useful if you just update the strategy)
void validate() {
final form = state.data;
emit(state.copyWith(status: validationStrategy.validate(form)));
}
/// Update entries list.
///
/// Inputs:
@@ -58,31 +75,36 @@ abstract class FormDataCubit extends Cubit<FormDataState> {
FormData data, {
SetOperation operation = SetOperation.replace,
}) {
FormData _form = data;
FormData form = data;
switch (operation) {
case SetOperation.replace:
_form = data;
form = data;
break;
case SetOperation.difference:
_form = state.data.difference(data);
form = state.data.difference(data);
break;
case SetOperation.intersection:
_form = state.data.intersection(data);
form = state.data.intersection(data);
break;
case SetOperation.union:
_form = state.data.union(data);
form = state.data.union(data);
break;
}
emit(
state.copyWith(
data: _form,
status: _form.selfValidate(),
data: form,
status: validationStrategy.validate(form),
),
);
}
/// Reset all form inputs
void resetForm() {
emit(FormDataState(data: formCopy));
}
/// Submit the form.
Future<void> submitForm();
}
@@ -16,10 +16,14 @@
part of 'form_data_cubit.dart';
@immutable
class FormDataState {
class FormDataState extends Equatable {
/// Global status of a form.
final FormStatus status;
/// FormData with all inputs, and associated metadata.
final FormData data;
/// Optional error message.
final String? errorMessage;
const FormDataState({
@@ -32,32 +36,15 @@ class FormDataState {
FormStatus? status,
FormData? data,
String? errorMessage,
}) {
return FormDataState(
}) => FormDataState(
status: status ?? this.status,
data: data ?? this.data,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is FormDataState &&
other.status == status &&
other.data == data &&
other.errorMessage == errorMessage;
}
bool? get stringify => true;
@override
int get hashCode {
return status.hashCode ^ data.hashCode ^ errorMessage.hashCode;
}
@override
String toString() {
return 'FormDataState(status: $status, data: $data, '
'errorMessage: $errorMessage)';
}
List<Object?> get props => [status, data, errorMessage];
}
@@ -14,6 +14,14 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const Set<FormStatus> _validatedFormStatuses = <FormStatus>{
FormStatus.valid,
FormStatus.submissionInProgress,
FormStatus.submissionSuccess,
FormStatus.submissionFailure,
FormStatus.submissionCanceled,
};
/// Enum representing the status of a form at any given point in time.
enum FormStatus {
/// The form has not been touched.
@@ -35,23 +43,14 @@ enum FormStatus {
submissionFailure,
/// The form submission has been canceled.
submissionCanceled
}
submissionCanceled;
const Set<FormStatus> _validatedFormStatuses = <FormStatus>{
FormStatus.valid,
FormStatus.submissionInProgress,
FormStatus.submissionSuccess,
FormStatus.submissionFailure,
FormStatus.submissionCanceled,
};
/// Useful extensions on [FormStatus]
extension FormStatusX on FormStatus {
/// Indicates whether the form is untouched.
bool get isPure => this == FormStatus.pure;
/// Indicates whether the form is completely validated.
/// This means the [FormStatus] is strictly:
/// * `FormStatus.valid`
bool get isValid => this == FormStatus.valid;
/// Indicates whether the form has been validated successfully.
@@ -14,7 +14,14 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
enum ValidationError {
/// Generic invalid error.
invalid
// enum ValidationError {
// /// Generic invalid error.
// invalid
// }
abstract class ValidationError {}
enum ValidationStandardError implements ValidationError {
invalid,
empty
}
@@ -14,6 +14,15 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'form_data.dart';
export 'form_entry.dart';
export 'form_input.dart';
import 'dart:convert';
import 'package:equatable/equatable.dart';
import 'package:wyatt_form_bloc/src/enums/form_input_status.dart';
import 'package:wyatt_form_bloc/src/enums/form_status.dart';
import 'package:wyatt_form_bloc/src/enums/validation_error.dart';
part 'form_data.dart';
part 'form_input.dart';
part 'form_input_metadata.dart';
part 'form_input_validator.dart';
part 'form_validator.dart';
@@ -14,133 +14,171 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:meta/meta.dart';
import 'package:wyatt_form_bloc/src/enums/enums.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
import 'package:wyatt_form_bloc/src/utils/list_equals.dart';
part of 'form.dart';
@immutable
class FormData {
const FormData(this._entries);
class FormData extends Equatable {
final List<FormInput> _inputs;
FormData.empty() : this(<FormEntry>[]);
const FormData(this._inputs);
const FormData.empty() : this(const <FormInput>[]);
final List<FormEntry> _entries;
/// Returns all inputs as a list
List<FormInput> inputs() => _inputs;
List<FormEntry> get entries => _entries;
List<FormInput<T, ValidationError>> inputs<T>() {
return _entries
.map((FormEntry entry) => entry.input as FormInput<T, ValidationError>)
.toList();
}
static FormStatus validate(List<FormInput> inputs) {
return inputs.every((FormInput element) => element.pure)
? FormStatus.pure
: inputs.any((FormInput input) => input.valid == false)
? FormStatus.invalid
: FormStatus.valid;
}
FormStatus selfValidate() {
return validate(inputs<dynamic>());
}
FormInput<T, ValidationError> input<T>(String key) {
/// Returns the input for the associated key
FormInput inputOf(String key) {
if (contains(key)) {
return _entries.firstWhere((FormEntry entry) => entry.key == key).input
as FormInput<T, ValidationError>;
return _inputs.firstWhere((input) => input.key == key);
} else {
throw Exception('FormInput with key `$key` does not exist in form');
}
}
bool contains(String key) {
return _entries.any((FormEntry entry) => entry.key == key);
}
FormData intersection(FormData other) {
final List<FormEntry> entries = <FormEntry>[];
for (final FormEntry entry in _entries) {
if (other.contains(entry.key)) {
entries.add(entry);
}
}
return FormData(entries);
}
FormData difference(FormData other) {
final List<FormEntry> entries = <FormEntry>[];
for (final FormEntry otherEntry in other._entries) {
if (!contains(otherEntry.key)) {
entries.add(otherEntry);
}
}
for (final FormEntry entry in _entries) {
if (!other.contains(entry.key)) {
entries.add(entry);
}
}
return FormData(entries);
}
FormData union(FormData other) {
final List<FormEntry> entries = <FormEntry>[];
for (final FormEntry entry in _entries) {
entries.add(entry);
}
for (final FormEntry otherEntry in other._entries) {
if (!contains(otherEntry.key)) {
entries.add(otherEntry);
}
}
return FormData(entries);
}
void update(String key, FormInput input) {
/// Updates a input (perform a replace at index).
void updateInput(String key, FormInput input) {
if (contains(key)) {
final index = _entries.indexOf(
_entries.firstWhere((FormEntry entry) => entry.key == key),
final index = _inputs.indexOf(
inputOf(key),
);
_entries[index] = _entries[index].copyWith(input: input);
_inputs[index] = input;
}
}
FormData clone() {
return FormData(
_entries.map((FormEntry entry) => entry.clone()).toList(),
);
/// Returns all associated validators as a list
List<FormInputValidator<V, E>> validators<V, E extends ValidationError>() =>
_inputs
.map<FormInputValidator<V, E>>(
(input) => input.validator as FormInputValidator<V, E>,
)
.toList();
/// A [FormInputValidator] represents the value of a single form input field.
/// It contains information about the [FormInputStatus], value, as well
/// as validation status.
T validatorOf<T>(String key) => inputOf(key).validator as T;
/// Updates validator of a given input. (perform copyWith)
void updateValidator<V>(
String key,
FormInputValidator<V, ValidationError> dirtyValue,
) {
if (contains(key)) {
final index = _inputs.indexOf(
inputOf(key),
);
_inputs[index] = _inputs[index].copyWith(validator: dirtyValue);
}
}
/// Returns a validation error if the [FormInputValidator] is invalid.
/// Returns null if the [FormInputValidator] is valid.
E? errorOf<E extends ValidationError>(String key) =>
(inputOf(key).validator as FormInputValidator<dynamic, E>).error;
/// The value of the associated [FormInputValidator]. For example,
/// if you have a FormInputValidator for FirstName, the value could be 'Joe'.
V valueOf<V>(String key) =>
(inputOf(key).validator as FormInputValidator<V, dynamic>).value;
/// Returns `true` if the [FormInputValidator] is not valid.
/// Same as `E? errorOf<V, E>(String key) != null`
bool isNotValid(String key) => !inputOf(key).validator.valid;
/// Returns all associated metadata as a list
List<FormInputMetadata<M>> metadata<M>() => _inputs
.map<FormInputMetadata<M>>(
(input) => input.metadata as FormInputMetadata<M>,
)
.toList();
/// Returns the metadata associated. With `M` the type of extra data.
FormInputMetadata<M> metadataOf<M>(String key) =>
inputOf(key).metadata as FormInputMetadata<M>;
/// Updates metadata of a given input. (perform copyWith)
void updateMetadata<M>(String key, FormInputMetadata<M> meta) {
if (contains(key)) {
final index = _inputs.indexOf(
inputOf(key),
);
_inputs[index] = _inputs[index].copyWith(metadata: meta);
}
}
/// Check if this contains an input with the given key.
bool contains(String key) => _inputs.any((input) => input.key == key);
/// Makes an intersection set operation and returns newly created [FormData]
FormData intersection(FormData other) {
final List<FormInput> inputs = <FormInput>[];
for (final FormInput i in _inputs) {
if (other.contains(i.key)) {
inputs.add(i);
}
}
return FormData(inputs);
}
/// Makes a difference set operation and returns newly created [FormData]
FormData difference(FormData other) {
final List<FormInput> inputs = <FormInput>[];
for (final FormInput i in other._inputs) {
if (!contains(i.key)) {
inputs.add(i);
}
}
for (final FormInput i in _inputs) {
if (!other.contains(i.key)) {
inputs.add(i);
}
}
return FormData(inputs);
}
/// Makes an union set operation and returns newly created [FormData]
FormData union(FormData other) {
final List<FormInput> inputs = <FormInput>[];
for (final FormInput i in _inputs) {
inputs.add(i);
}
for (final FormInput i in other._inputs) {
if (!contains(i.key)) {
inputs.add(i);
}
}
return FormData(inputs);
}
/// Deeply copy this.
FormData clone() => FormData(
_inputs.map((input) => input.clone()).toList(),
);
/// Export this to [Map] format.
Map<String, dynamic> toMap() {
final map = <String, dynamic>{};
for (final entry in _entries) {
if (entry.export) {
map[entry.name] = entry.input.value;
for (final input in _inputs) {
if (input.metadata.export) {
map[input.name] = input.validator.value;
}
}
return map;
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is FormData && listEquals(other._entries, _entries);
}
/// Export this to [String] format.
String toJson() => jsonEncode(toMap());
@override
int get hashCode => _entries.hashCode;
bool? get stringify => true;
@override
String toString() => 'FormData(entries: $_entries)';
List<Object?> get props => _inputs;
}
@@ -1,79 +0,0 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:meta/meta.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
@immutable
class FormEntry {
const FormEntry(this.key, this.input, {this.export = true, String? name})
: _field = name ?? key;
final String key;
final FormInput input;
final bool export;
final String _field;
String get name => _field;
FormEntry copyWith({
String? key,
FormInput? input,
bool? export,
String? name,
}) {
return FormEntry(
key ?? this.key,
input ?? this.input,
export: export ?? this.export,
name: name,
);
}
FormEntry clone() {
return FormEntry(
key,
input,
export: export,
name: name,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is FormEntry &&
other.key == key &&
other.input == input &&
other.export == export &&
other.name == name;
}
@override
int get hashCode {
return key.hashCode ^
input.hashCode ^
export.hashCode ^
name.hashCode;
}
@override
String toString() {
return 'FormEntry(key: $key, input: $input, '
'export: $export, name: $name)';
}
}
@@ -1,3 +1,4 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
@@ -14,98 +15,42 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:meta/meta.dart';
import 'package:wyatt_form_bloc/src/enums/enums.dart';
part of 'form.dart';
/// {@template form_input}
/// A [FormInput] represents the value of a single form input field.
/// It contains information about the [FormInputStatus], [value], as well
/// as validation status.
///
/// [FormInput] should be extended to define custom [FormInput] instances.
///
/// ```dart
/// enum FirstNameError { empty }
/// class FirstName extends FormInput<String, FirstNameError> {
/// const FirstName.pure({String value = ''}) : super.pure(value);
/// const FirstName.dirty({String value = ''}) : super.dirty(value);
///
/// @override
/// FirstNameError? validator(String value) {
/// return value.isEmpty ? FirstNameError.empty : null;
/// }
/// }
/// ```
/// {@endtemplate}
@immutable
abstract class FormInput<T, E> {
const FormInput._(this.value, [this.pure = true]);
class FormInput<Validator extends FormInputValidator<dynamic, ValidationError>>
extends Equatable {
final String key;
final Validator validator;
final FormInputMetadata<dynamic> metadata;
/// Constructor which create a `pure` [FormInput] with a given value.
const FormInput.pure(T value) : this._(value);
String get name => metadata._name ?? key;
/// Constructor which create a `dirty` [FormInput] with a given value.
const FormInput.dirty(T value) : this._(value, false);
const FormInput(
this.key,
this.validator, {
// ignore: avoid_redundant_argument_values
this.metadata = const FormInputMetadata<void>(export: true),
});
/// The value of the given [FormInput].
/// For example, if you have a `FormInput` for `FirstName`,
/// the value could be 'Joe'.
final T value;
FormInput copyWith({
String? key,
Validator? validator,
FormInputMetadata? metadata,
}) =>
FormInput(
key ?? this.key,
validator ?? this.validator,
metadata: metadata ?? this.metadata,
);
/// If the [FormInput] is pure (has been touched/modified).
/// Typically when the `FormInput` is initially created,
/// it is created using the `FormInput.pure` constructor to
/// signify that the user has not modified it.
///
/// For subsequent changes (in response to user input), the
/// `FormInput.dirty` constructor should be used to signify that
/// the `FormInput` has been manipulated.
final bool pure;
/// The [FormInputStatus] which can be one of the following:
/// * [FormInputStatus.pure]
/// - if the input has not been modified.
/// * [FormInputStatus.invalid]
/// - if the input has been modified and validation failed.
/// * [FormInputStatus.valid]
/// - if the input has been modified and validation succeeded.
FormInputStatus get status => pure
? FormInputStatus.pure
: valid
? FormInputStatus.valid
: FormInputStatus.invalid;
/// Returns a validation error if the [FormInput] is invalid.
/// Returns `null` if the [FormInput] is valid.
E? get error => validator(value);
/// Whether the [FormInput] value is valid according to the
/// overridden `validator`.
///
/// Returns `true` if `validator` returns `null` for the
/// current [FormInput] value and `false` otherwise.
bool get valid => validator(value) == null;
/// Whether the [FormInput] value is not valid.
/// A value is invalid when the overridden `validator`
/// returns an error (non-null value).
bool get invalid => status == FormInputStatus.invalid;
/// A function that must return a validation error if the provided
/// [value] is invalid and `null` otherwise.
E? validator(T value);
FormInput clone() => copyWith(
key: key,
validator: validator,
metadata: metadata,
);
@override
bool? get stringify => true;
@override
int get hashCode => value.hashCode ^ pure.hashCode;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
return other is FormInput<T, E> &&
other.value == value &&
other.pure == pure;
}
@override
String toString() => 'FormInput<$runtimeType>(value: $value, pure: $pure)';
List<Object?> get props => [key, validator, metadata];
}
@@ -0,0 +1,53 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
part of 'form.dart';
class FormInputMetadata<T extends Object?> extends Equatable {
final bool export;
final String? _name;
final T? extra;
const FormInputMetadata({
this.export = true,
this.extra,
String? name,
}) : _name = name;
FormInputMetadata<T> copyWith({
bool? export,
String? name,
T? extra,
}) =>
FormInputMetadata<T>(
export: export ?? this.export,
name: name ?? _name,
extra: extra ?? this.extra,
);
FormInputMetadata<T> clone() => copyWith(
export: export,
name: _name,
extra: extra,
);
@override
bool? get stringify => true;
@override
List<Object?> get props => [export, _name, extra];
}
@@ -0,0 +1,106 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
part of 'form.dart';
/// {@template form_input_validator}
/// A [FormInputValidator] represents the value of a single form input field.
/// It contains information about the [FormInputStatus], [value], as well
/// as validation status.
///
/// [FormInputValidator] should be extended to define custom
/// [FormInputValidator] instances.
///
/// ```dart
/// class Name extends FormInputValidator<String, ValidationStandardError> {
/// const Name.pure({String value = ''}) : super.pure(value);
/// const Name.dirty({String value = ''}) : super.dirty(value);
///
/// @override
/// ValidationStandardError? validator(String? value) {
/// if (value?.isEmpty ?? false) {
/// return ValidationStandardError.empty
/// }
/// return value == null ? ValidationStandardError.invalid : null;
/// }
/// }
/// ```
/// {@endtemplate}
abstract class FormInputValidator<V, E extends ValidationError>
extends Equatable {
const FormInputValidator._(this.value, [this.pure = true]);
/// Constructor which create a `pure` [FormInputValidator] with a given value.
const FormInputValidator.pure(V value) : this._(value);
/// Constructor which create a `dirty` [FormInputValidator] with a
/// given value.
const FormInputValidator.dirty(V value) : this._(value, false);
/// The value of the given [FormInputValidator].
/// For example, if you have a `FormInputValidator` for `FirstName`,
/// the value could be 'Joe'.
final V value;
/// If the [FormInputValidator] is pure (has been touched/modified).
/// Typically when the `FormInputValidator` is initially created,
/// it is created using the `FormInputValidator.pure` constructor to
/// signify that the user has not modified it.
///
/// For subsequent changes (in response to user input), the
/// `FormInputValidator.dirty` constructor should be used to signify that
/// the `FormInputValidator` has been manipulated.
final bool pure;
/// The [FormInputStatus] which can be one of the following:
/// * [FormInputStatus.pure]
/// - if the input has not been modified.
/// * [FormInputStatus.invalid]
/// - if the input has been modified and validation failed.
/// * [FormInputStatus.valid]
/// - if the input has been modified and validation succeeded.
FormInputStatus get status => pure
? FormInputStatus.pure
: valid
? FormInputStatus.valid
: FormInputStatus.invalid;
/// Returns a validation error if the [FormInputValidator] is invalid.
/// Returns `null` if the [FormInputValidator] is valid.
E? get error => validator(value);
/// Whether the [FormInputValidator] value is valid according to the
/// overridden `validator`.
///
/// Returns `true` if `validator` returns `null` for the
/// current [FormInputValidator] value and `false` otherwise.
bool get valid => validator(value) == null;
/// Whether the [FormInputValidator] value is not valid.
/// A value is invalid when the overridden `validator`
/// returns an error (non-null value).
bool get invalid => status == FormInputStatus.invalid;
/// A function that must return a validation error if the provided
/// [value] is invalid and `null` otherwise.
E? validator(V value);
@override
bool? get stringify => true;
@override
List<Object?> get props => [value, pure];
}
@@ -14,21 +14,18 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_http_client/src/authentication/interfaces/header_authentication_client.dart';
part of 'form.dart';
typedef TokenParser = String Function(Map<String, dynamic>);
/// {@template form_validator}
/// A [FormValidator] represents the global validaton state of a Form.
/// {@endtemplate}
abstract class FormValidator {
/// {@macro form_validator}
const FormValidator();
abstract class Oauth2Client extends HeaderAuthenticationClient {
Oauth2Client(super.inner);
bool isPure(FormData form) => form
.validators<dynamic, ValidationError>()
.every((validator) => validator.pure);
Future<void> refresh() {
return Future.value();
}
Future<void> authorize(
Map<String, dynamic> body, {
Map<String, String>? headers,
}) {
return Future.value();
}
FormStatus validate(FormData form);
}
@@ -0,0 +1,40 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
/// {@template every}
/// Check and validate every input of a form
/// {@endtemplate}
class EveryInputValidator extends FormValidator {
/// {@macro every}
const EveryInputValidator() : super();
@override
FormStatus validate(FormData form) {
if (isPure(form)) {
return FormStatus.pure;
}
if (form
.validators<dynamic, ValidationError>()
.any((validator) => validator.valid == false)) {
return FormStatus.invalid;
}
return FormStatus.valid;
}
}
@@ -0,0 +1,41 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/validation_error.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
/// {@template regex_validator}
/// Abstract regex validator for form input.
/// {@endtemplate}
abstract class RegexValidator<E extends ValidationError>
extends FormInputValidator<String, E> {
const RegexValidator.pure() : super.pure('');
const RegexValidator.dirty([super.value = '']) : super.dirty();
RegExp get regex;
/// If the value is **not** null, but empty.
E get onEmpty;
/// If value does not conform to regex.
E get onError;
@override
E? validator(String? value) {
if (value?.isEmpty ?? false) {
return onEmpty;
}
return regex.hasMatch(value ?? '') ? null : onError;
}
}
@@ -0,0 +1,38 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/validation_error.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
abstract class TextValidator<E extends ValidationError>
extends FormInputValidator<String, E> {
const TextValidator.pure() : super.pure('');
const TextValidator.dirty([super.value = '']) : super.dirty();
/// If the value is **not** null, but empty.
E get onEmpty;
/// If value is null.
E get onNull;
@override
E? validator(String? value) {
if (value?.isEmpty ?? false) {
return onEmpty;
}
return value != null ? null : onNull;
}
}
@@ -20,7 +20,7 @@ import 'package:wyatt_form_bloc/src/form/form.dart';
/// {@template boolean}
/// Form input for a bool input
/// {@endtemplate}
class Boolean extends FormInput<bool, ValidationError> {
class Boolean extends FormInputValidator<bool, ValidationStandardError> {
/// {@macro boolean}
const Boolean.pure({bool? defaultValue = false})
: super.pure(defaultValue ?? false);
@@ -29,7 +29,6 @@ class Boolean extends FormInput<bool, ValidationError> {
const Boolean.dirty({bool value = false}) : super.dirty(value);
@override
ValidationError? validator(bool? value) {
return value != null ? null : ValidationError.invalid;
}
ValidationStandardError? validator(bool? value) =>
value != null ? null : ValidationStandardError.invalid;
}
@@ -21,7 +21,7 @@ import 'package:wyatt_form_bloc/src/form/form.dart';
/// Form input for a confirmed password input.
/// {@endtemplate}
class ConfirmedPassword
extends FormInput<String, ValidationError> {
extends FormInputValidator<String, ValidationStandardError> {
/// {@macro confirmed_password}
const ConfirmedPassword.pure({this.password = ''}) : super.pure('');
@@ -33,7 +33,6 @@ class ConfirmedPassword
final String password;
@override
ValidationError? validator(String? value) {
return password == value ? null : ValidationError.invalid;
}
ValidationStandardError? validator(String? value) =>
password == value ? null : ValidationStandardError.invalid;
}
@@ -15,24 +15,25 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
import 'package:wyatt_form_bloc/src/validators/inputs/base/regex_validator.dart';
/// {@template email}
/// Form input for an email input.
/// {@endtemplate}
class Email extends FormInput<String, ValidationError> {
class Email extends RegexValidator<ValidationStandardError> {
/// {@macro email}
const Email.pure() : super.pure('');
const Email.pure() : super.pure();
/// {@macro email}
const Email.dirty([String value = '']) : super.dirty(value);
static final RegExp _emailRegExp = RegExp(
r'^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$',
);
const Email.dirty([super.value = '']) : super.dirty();
@override
ValidationError? validator(String? value) {
return _emailRegExp.hasMatch(value ?? '') ? null : ValidationError.invalid;
}
ValidationStandardError get onEmpty => ValidationStandardError.empty;
@override
ValidationStandardError get onError => ValidationStandardError.invalid;
@override
RegExp get regex => RegExp(
r'[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+',
);
}
@@ -14,24 +14,19 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:http/http.dart';
import 'package:wyatt_http_client/src/rest_client.dart';
import 'package:wyatt_form_bloc/src/enums/validation_error.dart';
import 'package:wyatt_form_bloc/src/validators/inputs/base/text_validator.dart';
abstract class AuthenticationClient extends BaseClient {
final BaseClient _inner;
class EnumValidator<E> extends TextValidator<ValidationStandardError> {
/// {@macro text_string}
const EnumValidator.pure() : super.pure();
BaseClient get inner => _inner;
AuthenticationClient(BaseClient? inner) : _inner = inner ?? RestClient();
/// {@macro text_string}
EnumValidator.dirty(E value) : super.dirty(value.toString());
@override
Future<StreamedResponse> send(BaseRequest request) {
return _inner.send(request);
}
ValidationStandardError get onEmpty => ValidationStandardError.empty;
@override
void close() {
_inner.close();
return super.close();
}
ValidationStandardError get onNull => ValidationStandardError.invalid;
}
@@ -15,24 +15,25 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
import 'package:wyatt_form_bloc/src/validators/inputs/base/regex_validator.dart';
/// {@template iban}
/// Form input for an IBAN input.
/// {@endtemplate}
class Iban extends FormInput<String, ValidationError> {
class Iban extends RegexValidator<ValidationStandardError> {
/// {@macro iban}
const Iban.pure() : super.pure('');
const Iban.pure() : super.pure();
/// {@macro iban}
const Iban.dirty([String value = '']) : super.dirty(value);
static final RegExp _regExp = RegExp(
r'^(?:((?:IT|SM)\d{2}[A-Z]{1}\d{22})|(NL\d{2}[A-Z]{4}\d{10})|(LV\d{2}[A-Z]{4}\d{13})|((?:BG|GB|IE)\d{2}[A-Z]{4}\d{14})|(GI\d{2}[A-Z]{4}\d{15})|(RO\d{2}[A-Z]{4}\d{16})|(MT\d{2}[A-Z]{4}\d{23})|(NO\d{13})|((?:DK|FI)\d{16})|((?:SI)\d{17})|((?:AT|EE|LU|LT)\d{18})|((?:HR|LI|CH)\d{19})|((?:DE|VA)\d{20})|((?:AD|CZ|ES|MD|SK|SE)\d{22})|(PT\d{23})|((?:IS)\d{24})|((?:BE)\d{14})|((?:FR|MC|GR)\d{25})|((?:PL|HU|CY)\d{26}))$',
);
const Iban.dirty([super.value = '']) : super.dirty();
@override
ValidationError? validator(String? value) {
return _regExp.hasMatch(value ?? '') ? null : ValidationError.invalid;
}
ValidationStandardError get onEmpty => ValidationStandardError.empty;
@override
ValidationStandardError get onError => ValidationStandardError.invalid;
@override
RegExp get regex => RegExp(
r'^(?:((?:IT|SM)\d{2}[A-Z]{1}\d{22})|(NL\d{2}[A-Z]{4}\d{10})|(LV\d{2}[A-Z]{4}\d{13})|((?:BG|GB|IE)\d{2}[A-Z]{4}\d{14})|(GI\d{2}[A-Z]{4}\d{15})|(RO\d{2}[A-Z]{4}\d{16})|(MT\d{2}[A-Z]{4}\d{23})|(NO\d{13})|((?:DK|FI)\d{16})|((?:SI)\d{17})|((?:AT|EE|LU|LT)\d{18})|((?:HR|LI|CH)\d{19})|((?:DE|VA)\d{20})|((?:AD|CZ|ES|MD|SK|SE)\d{22})|(PT\d{23})|((?:IS)\d{24})|((?:BE)\d{14})|((?:FR|MC|GR)\d{25})|((?:PL|HU|CY)\d{26}))$',
);
}
@@ -20,7 +20,8 @@ import 'package:wyatt_form_bloc/src/form/form.dart';
/// {@template list_option}
/// Form input for a list input
/// {@endtemplate}
class ListOption<T> extends FormInput<List<T>, ValidationError> {
class ListOption<T>
extends FormInputValidator<List<T>, ValidationStandardError> {
/// {@macro list_option}
const ListOption.pure({List<T>? defaultValue})
: super.pure(defaultValue ?? const []);
@@ -28,7 +29,7 @@ class ListOption<T> extends FormInput<List<T>, ValidationError> {
/// {@macro list_option}
const ListOption.dirty({List<T>? value}) : super.dirty(value ?? const []);
ListOption select(T? v) {
ListOption<T> select(T? v) {
if (v == null) {
return this;
}
@@ -42,7 +43,8 @@ class ListOption<T> extends FormInput<List<T>, ValidationError> {
}
@override
ValidationError? validator(List<T>? value) {
return value?.isNotEmpty == true ? null : ValidationError.invalid;
}
ValidationStandardError? validator(List<T>? value) =>
value?.isNotEmpty ?? false == true
? null
: ValidationStandardError.invalid;
}
@@ -15,22 +15,23 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
import 'package:wyatt_form_bloc/src/validators/inputs/base/regex_validator.dart';
/// {@template name}
/// Form input for a name input.
/// {@endtemplate}
class Name extends FormInput<String, ValidationError> {
class Name extends RegexValidator<ValidationStandardError> {
/// {@macro name}
const Name.pure() : super.pure('');
const Name.pure() : super.pure();
/// {@macro name}
const Name.dirty([String value = '']) : super.dirty(value);
static final RegExp _nameRegExp = RegExp(r"^([ \u00c0-\u01ffa-zA-Z'\-])+$");
const Name.dirty([super.value = '']) : super.dirty();
@override
ValidationError? validator(String? value) {
return _nameRegExp.hasMatch(value ?? '') ? null : ValidationError.invalid;
}
ValidationStandardError get onEmpty => ValidationStandardError.empty;
@override
ValidationStandardError get onError => ValidationStandardError.invalid;
@override
RegExp get regex => RegExp(r"^([ \u00c0-\u01ffa-zA-Z'\-])+$");
}
@@ -15,25 +15,23 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
import 'package:wyatt_form_bloc/src/validators/inputs/base/regex_validator.dart';
/// {@template password}
/// Form input for a password input.
/// {@endtemplate}
class Password extends FormInput<String, ValidationError> {
class Password extends RegexValidator<ValidationStandardError> {
/// {@macro password}
const Password.pure() : super.pure('');
const Password.pure() : super.pure();
/// {@macro password}
const Password.dirty([String value = '']) : super.dirty(value);
static final RegExp _passwordRegExp =
RegExp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$');
const Password.dirty([super.value = '']) : super.dirty();
@override
ValidationError? validator(String? value) {
return _passwordRegExp.hasMatch(value ?? '')
? null
: ValidationError.invalid;
}
ValidationStandardError get onEmpty => ValidationStandardError.empty;
@override
ValidationStandardError get onError => ValidationStandardError.invalid;
@override
RegExp get regex => RegExp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$');
}
@@ -15,23 +15,24 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
import 'package:wyatt_form_bloc/src/validators/inputs/base/regex_validator.dart';
/// {@template phone}
/// Form input for a phone input.
/// {@endtemplate}
class Phone extends FormInput<String, ValidationError> {
class Phone extends RegexValidator<ValidationStandardError> {
/// {@macro phone}
const Phone.pure() : super.pure('');
const Phone.pure() : super.pure();
/// {@macro phone}
const Phone.dirty([String value = '']) : super.dirty(value);
static final RegExp _phoneRegExp =
RegExp(r'^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$');
const Phone.dirty([super.value = '']) : super.dirty();
@override
ValidationError? validator(String? value) {
return _phoneRegExp.hasMatch(value ?? '') ? null : ValidationError.invalid;
}
ValidationStandardError get onEmpty => ValidationStandardError.empty;
@override
ValidationStandardError get onError => ValidationStandardError.invalid;
@override
RegExp get regex =>
RegExp(r'^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$');
}
@@ -15,22 +15,23 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
import 'package:wyatt_form_bloc/src/validators/inputs/base/regex_validator.dart';
/// {@template siren}
/// Form input for a SIREN input.
/// {@endtemplate}
class Siren extends FormInput<String, ValidationError> {
class Siren extends RegexValidator<ValidationStandardError> {
/// {@macro siren}
const Siren.pure() : super.pure('');
const Siren.pure() : super.pure();
/// {@macro siren}
const Siren.dirty([String value = '']) : super.dirty(value);
static final RegExp _regExp = RegExp(r'(\d{9}|\d{3}[ ]\d{3}[ ]\d{3})$');
const Siren.dirty([super.value = '']) : super.dirty();
@override
ValidationError? validator(String? value) {
return _regExp.hasMatch(value ?? '') ? null : ValidationError.invalid;
}
ValidationStandardError get onEmpty => ValidationStandardError.empty;
@override
ValidationStandardError get onError => ValidationStandardError.invalid;
@override
RegExp get regex => RegExp(r'(\d{9}|\d{3}[ ]\d{3}[ ]\d{3})$');
}
@@ -15,20 +15,21 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
import 'package:wyatt_form_bloc/src/validators/inputs/base/text_validator.dart';
/// {@template text_string}
/// Form input for a text input
/// {@endtemplate}
class TextString extends FormInput<String, ValidationError> {
class TextString extends TextValidator<ValidationStandardError> {
/// {@macro text_string}
const TextString.pure() : super.pure('');
const TextString.pure() : super.pure();
/// {@macro text_string}
const TextString.dirty([String value = '']) : super.dirty(value);
const TextString.dirty([super.value = '']) : super.dirty();
@override
ValidationError? validator(String? value) {
return (value?.isNotEmpty ?? false) ? null : ValidationError.invalid;
}
ValidationStandardError get onEmpty => ValidationStandardError.empty;
@override
ValidationStandardError get onNull => ValidationStandardError.invalid;
}
@@ -14,13 +14,16 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'boolean.dart';
export 'confirmed_password.dart';
export 'email.dart';
export 'iban.dart';
export 'list_option.dart';
export 'name.dart';
export 'password.dart';
export 'phone.dart';
export 'siren.dart';
export 'text_string.dart';
export 'inputs/base/regex_validator.dart';
export 'inputs/base/text_validator.dart';
export 'inputs/boolean.dart';
export 'inputs/confirmed_password.dart';
export 'inputs/email.dart';
export 'inputs/enum_validator.dart';
export 'inputs/iban.dart';
export 'inputs/list_option.dart';
export 'inputs/name.dart';
export 'inputs/password.dart';
export 'inputs/phone.dart';
export 'inputs/siren.dart';
export 'inputs/text_string.dart';
+5 -5
View File
@@ -1,20 +1,20 @@
name: wyatt_form_bloc
description: Manage forms in Dart & Flutter with Bloc
repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_form_bloc
version: 0.0.2
version: 0.0.4
environment:
sdk: '>=2.16.2 <3.0.0'
sdk: '>=2.17.2 <3.0.0'
dependencies:
bloc: ^8.0.3
meta: ^1.7.0
equatable: ^2.0.3
dev_dependencies:
test: ^1.21.0
test: ^1.21.4
wyatt_analysis:
git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_analysis-v2.1.0
ref: wyatt_analysis-v2.2.1
path: packages/wyatt_analysis
+7
View File
@@ -1,3 +1,10 @@
## 1.2.0
- **FEAT**: add new middleware feature.
- **FEAT**: implements doublelinked list for middlewares.
- **FEAT**: [WIP] implements middleware system.
- **FEAT**: [WIP] work on middleware feature.
## 1.1.0
- **FEAT**: add oauth2 refresh token client.
+164 -21
View File
@@ -1,39 +1,182 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
* Copyright (C) 2022 WYATT GROUP
* Please see the AUTHORS file for details.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
# Dart - HTTP Client
## Features
<p align="left">
<a href="https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_analysis">
<img src="https://img.shields.io/badge/Style-Wyatt%20Analysis-blue.svg?style=flat-square" alt="Style: Wyatt Analysis" />
</a>
<img src="https://img.shields.io/badge/SDK-Dart%20%7C%20Flutter-blue?style=flat-square" alt="SDK: Dart & Flutter" />
</p>
TODO: List what your package can do. Maybe include images, gifs, or videos.
HTTP Client for Dart with Middlewares !
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
Simply add wyatt_http_client in pubspec.yaml, then
```dart
import 'package:wyatt_http_client/wyatt_http_client.dart';
```
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
Firstly you have to understand **Middleware** and **Pipeline** concepts.
In `wyatt_http_client` a middleware is an object where requests and responses
pass through. And a pipeline is basicaly a list of middlewares.
In a pipeline with middlewares A and B, if request pass through A, then B,
the response will pass through B then A.
> You can `print(pipeline)` to get full process order of a pipeline.
For example, if you want to log every request, and simplify an url you can use provided `SimpleLogger` and `UriPrefix` .
```dart
const like = 'sample';
// Create the Pipeline
final Pipeline pipeline = Pipeline()
.addMiddleware(
UriPrefixMiddleware(
protocol: Protocols.http,
authority: 'localhost:80',
),
)
.addMiddleware(SimpleLoggerMiddleware());
```
## Additional information
Then if you print the pipeline,
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.
```
[Req] -> UriPrefix -> SimpleLogger
[Res] -> SimpleLogger
```
> The `response` doesn't pass through `UriPrefix` because it's an `OnRequestMiddleware` only.
And you can create a client.
```dart
final client = MiddlewareClient(pipeline: pipeline);
```
At this point you can use `client` like every Client from `package:http/http.dart` .
## Recipes
### Rest API with URL Authentication
Let's build a client for a REST API where the (bad) authentication is through the URL.
We need some middlewares:
* SimpleLogger, to log every request and response (useful for debug).
* BodyToJson, to automaticaly transform Map object to JSON.
* UriPrefix, to simplify the build of an API Object (save protocol and API prefix).
* UnsafeAuth, to use url based authentication.
Let's start by creating the Pipeline:
```dart
final Pipeline pipeline = Pipeline()
.addMiddleware(
UriPrefixMiddleware(
protocol: Protocols.http,
authority: 'localhost:80',
),
)
.addMiddleware(BodyToJsonMiddleware())
.addMiddleware(
UnsafeAuthMiddleware(
username: 'wyatt',
password: 'motdepasse',
),
)
.addMiddleware(SimpleLoggerMiddleware());
```
Then simply create a client and make a call.
```dart
final client = MiddlewareClient(pipeline: pipeline);
await client.get(Uri.parse('/protected'));
```
> Here it make a `GET` call on `http://localhost:80/protected?username=wyatt&password=motdepasse`
And voilà.
### Rest API with Oauth2
So now we want a real authentication.
```dart
final Pipeline pipeline = Pipeline()
.addMiddleware(
UriPrefixMiddleware(
protocol: Protocols.http,
authority: 'localhost:80',
),
)
.addMiddleware(BodyToJsonMiddleware())
.addMiddleware(
RefreshTokenAuthMiddleware(
authorizationEndpoint: '/auth/sign-in',
tokenEndpoint: '/auth/refresh',
accessTokenParser: (body) => body['access_token']! as String,
refreshTokenParser: (body) => body['refresh_token']! as String,
unauthorized: HttpStatus.forbidden,
),
)
.addMiddleware(SimpleLoggerMiddleware());
```
> Here we just change `UnsafeAuthMiddleware` by `RefreshTokenAuthMiddleware` and the whole app while adapt to a new authentication system.
### Create a new Middleware
You can create your own middleware by implementing `Middleware` class, and use mixins to add `OnRequest` and/or `OnResponse` methods.
```dart
class SimpleLoggerMiddleware
with OnRequestMiddleware, OnResponseMiddleware
implements Middleware {
@override
String getName() => 'SimpleLogger';
@override
Future<MiddlewareRequest> onRequest(
MiddlewareContext context,
MiddlewareRequest request,
) async {
print('${getName()}::OnRequest');
return request;
}
@override
Future<MiddlewareResponse> onResponse(
MiddlewareContext context,
MiddlewareResponse response,
) async {
print('${getName()}::OnResponse');
return response;
}
}
```
@@ -17,14 +17,7 @@
import 'dart:async';
import 'dart:io';
import 'package:wyatt_http_client/src/authentication/basic_authentication_client.dart';
import 'package:wyatt_http_client/src/authentication/bearer_authentication_client.dart';
import 'package:wyatt_http_client/src/authentication/digest_authentication_client.dart';
import 'package:wyatt_http_client/src/authentication/refresh_token_client.dart';
import 'package:wyatt_http_client/src/authentication/unsafe_authentication_client.dart';
import 'package:wyatt_http_client/src/rest_client.dart';
import 'package:wyatt_http_client/src/utils/header_keys.dart';
import 'package:wyatt_http_client/src/utils/protocols.dart';
import 'package:wyatt_http_client/wyatt_http_client.dart';
String lastToken = '';
int token = 0;
@@ -42,7 +35,7 @@ Future<void> handleBasic(HttpRequest req) async {
Future<void> handleBasicNegotiate(HttpRequest req) async {
if (req.headers.value('Authorization') == null) {
req.response.statusCode = HttpStatus.unauthorized;
req.response.statusCode = HttpStatus.unauthorized.statusCode;
req.response.headers.set(HeaderKeys.wwwAuthenticate, 'Basic realm="Wyatt"');
print(req.response.headers.value('WWW-Authenticate'));
return req.response.close();
@@ -56,7 +49,7 @@ Future<void> handleBearer(HttpRequest req) async {
Future<void> handleDigest(HttpRequest req) async {
if (req.headers.value('Authorization') == null) {
req.response.statusCode = HttpStatus.unauthorized;
req.response.statusCode = HttpStatus.unauthorized.statusCode;
req.response.headers.set(
'WWW-Authenticate',
'Digest realm="Wyatt", '
@@ -110,7 +103,7 @@ Future<void> handleOauth2RefreshToken(HttpRequest req) async {
return req.response.close();
} else {
lastToken = receivedToken;
req.response.statusCode = HttpStatus.unauthorized;
req.response.statusCode = HttpStatus.unauthorized.statusCode;
return req.response.close();
}
default:
@@ -160,13 +153,13 @@ Future<void> server() async {
print('Authorized');
error = 0;
} else {
request.response.statusCode = HttpStatus.unauthorized;
request.response.statusCode = HttpStatus.unauthorized.statusCode;
}
break;
case '/test/oauth2-test-timeout':
error++;
print('Error $error');
request.response.statusCode = HttpStatus.unauthorized;
request.response.statusCode = HttpStatus.unauthorized.statusCode;
break;
case '/test/oauth2-login':
if (request.method == 'POST') {
@@ -189,12 +182,12 @@ Future<void> server() async {
}
break;
case '/test/oauth2-refresh-error':
request.response.statusCode = HttpStatus.unauthorized;
request.response.statusCode = HttpStatus.unauthorized.statusCode;
break;
default:
print(' => Unknown path or method');
request.response.statusCode = HttpStatus.notFound;
request.response.statusCode = HttpStatus.notFound.statusCode;
}
request.response.close();
print('====================');
@@ -204,73 +197,98 @@ Future<void> server() async {
Future<void> main() async {
unawaited(server());
final base = 'localhost:8080';
final restClient = RestClient(protocol: Protocols.http, authority: base);
final uriPrefix = UriPrefixMiddleware(
protocol: Protocols.http,
authority: base,
);
final jsonEncoder = BodyToJsonMiddleware();
final logger = SimpleLoggerMiddleware();
// Basic
final basic = BasicAuthenticationClient(
final basicAuth = BasicAuthMiddleware(
username: 'username',
password: 'password',
inner: restClient,
);
final basic = MiddlewareClient(
pipeline: Pipeline.fromIterable([
uriPrefix,
basicAuth,
logger,
]),
);
await basic.get(Uri.parse('/test/basic-test'));
// Basic with negotiate
final basicWithNegotiate = BasicAuthenticationClient(
username: 'username',
password: 'password',
preemptive: false,
inner: restClient,
);
await basicWithNegotiate.get(Uri.parse('/test/basic-test-with-negotiate'));
// Digest
final digest = DigestAuthenticationClient(
final digestAuth = DigestAuthMiddleware(
username: 'Mufasa',
password: 'Circle Of Life',
inner: restClient,
);
final digest = MiddlewareClient(
pipeline: Pipeline.fromIterable([
uriPrefix,
digestAuth,
logger,
]),
);
await digest.get(Uri.parse('/test/digest-test'));
// Bearer
final bearer = BearerAuthenticationClient(
token: 'access-token-test',
inner: restClient,
);
await bearer.get(Uri.parse('/test/bearer-test'));
// // Bearer
// final bearer = BearerAuthenticationClient(
// token: 'access-token-test',
// inner: restClient,
// );
// await bearer.get(Uri.parse('/test/bearer-test'));
// API Key
final apiKey = BearerAuthenticationClient(
token: 'awesome-api-key',
authenticationMethod: 'ApiKey',
inner: restClient,
);
await apiKey.get(Uri.parse('/test/apikey-test'));
// // API Key
// final apiKey = BearerAuthenticationClient(
// token: 'awesome-api-key',
// authenticationMethod: 'ApiKey',
// inner: restClient,
// );
// await apiKey.get(Uri.parse('/test/apikey-test'));
// Unsafe URL
final unsafe = UnsafeAuthenticationClient(
final unsafeAuth = UnsafeAuthMiddleware(
username: 'Mufasa',
password: 'Circle Of Life',
inner: restClient,
);
final unsafe = MiddlewareClient(
pipeline: Pipeline.fromIterable([
uriPrefix,
unsafeAuth,
logger,
]),
);
await unsafe.get(Uri.parse('/test/unsafe-test'));
// OAuth2
final refreshToken = RefreshTokenClient(
final refreshTokenAuth = RefreshTokenAuthMiddleware(
authorizationEndpoint: '/test/oauth2-test?action=login',
tokenEndpoint: '/test/oauth2-test?action=refresh',
accessTokenParser: (body) => body['accessToken']! as String,
refreshTokenParser: (body) => body['refreshToken']! as String,
inner: restClient,
);
final refreshToken = MiddlewareClient(
pipeline: Pipeline.fromIterable([
uriPrefix,
jsonEncoder,
refreshTokenAuth,
logger,
]),
);
await refreshToken.get(Uri.parse('/test/oauth2-test'));
await refreshToken.authorize(<String, String>{
'username': 'username',
'password': 'password',
});
// Login
await refreshToken.post(
Uri.parse('/test/oauth2-test'),
body: <String, String>{
'username': 'username',
'password': 'password',
},
);
await refreshToken.get(Uri.parse('/test/oauth2-test'));
await refreshToken.refresh();
await refreshToken.get(Uri.parse('/test/oauth2-test'));
await refreshToken.get(Uri.parse('/test/oauth2-test?action=access-denied'));
// await refreshToken.refresh();
// await refreshToken.get(Uri.parse('/test/oauth2-test'));
// await refreshToken.get(Uri.parse('/test/oauth2-test?action=access-denied'));
exit(0);
}
@@ -0,0 +1,398 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
import 'package:wyatt_http_client/src/middleware_client.dart';
import 'package:wyatt_http_client/src/middlewares/body_to_json_middleware.dart';
import 'package:wyatt_http_client/src/middlewares/refresh_token_auth_middleware.dart';
import 'package:wyatt_http_client/src/middlewares/simple_logger_middleware.dart';
import 'package:wyatt_http_client/src/middlewares/uri_prefix_middleware.dart';
import 'package:wyatt_http_client/src/pipeline.dart';
import 'package:wyatt_http_client/src/utils/http_status.dart';
import 'package:wyatt_http_client/src/utils/protocols.dart';
enum EmailVerificationAction {
signUp,
resetPassword,
changeEmail;
String toSnakeCase() {
return name.splitMapJoin(
RegExp('[A-Z]'),
onMatch: (m) => '_${m[0]?.toLowerCase()}',
onNonMatch: (n) => n,
);
}
factory EmailVerificationAction.fromString(String str) {
return EmailVerificationAction.values.firstWhere(
(EmailVerificationAction element) => element.toSnakeCase() == str,
);
}
}
class VerifyCode {
final String email;
final String verificationCode;
final EmailVerificationAction action;
VerifyCode({
required this.email,
required this.verificationCode,
required this.action,
});
VerifyCode copyWith({
String? email,
String? verificationCode,
EmailVerificationAction? action,
}) {
return VerifyCode(
email: email ?? this.email,
verificationCode: verificationCode ?? this.verificationCode,
action: action ?? this.action,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'email': email,
'verification_code': verificationCode,
'action': action.toSnakeCase(),
};
}
factory VerifyCode.fromMap(Map<String, dynamic> map) {
return VerifyCode(
email: map['email'] as String,
verificationCode: map['verification_code'] as String,
action: EmailVerificationAction.fromString(map['action'] as String),
);
}
String toJson() => json.encode(toMap());
factory VerifyCode.fromJson(String source) =>
VerifyCode.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() =>
'VerifyCode(email: $email, verificationCode: $verificationCode, action: $action)';
}
class Account {
final String email;
final String? sessionId;
Account({
required this.email,
this.sessionId,
});
Account copyWith({
String? email,
String? sessionId,
}) {
return Account(
email: email ?? this.email,
sessionId: sessionId ?? this.sessionId,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'email': email,
'session_id': sessionId,
};
}
factory Account.fromMap(Map<String, dynamic> map) {
return Account(
email: map['email'] as String,
sessionId: map['session_id'] != null ? map['session_id'] as String : null,
);
}
String toJson() => json.encode(toMap());
factory Account.fromJson(String source) =>
Account.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() => 'Account(email: $email, sessionId: $sessionId)';
}
class SignUp {
final String sessionId;
final String password;
SignUp({
required this.sessionId,
required this.password,
});
SignUp copyWith({
String? sessionId,
String? password,
}) {
return SignUp(
sessionId: sessionId ?? this.sessionId,
password: password ?? this.password,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'session_id': sessionId,
'password': password,
};
}
factory SignUp.fromMap(Map<String, dynamic> map) {
return SignUp(
sessionId: map['session_id'] as String,
password: map['password'] as String,
);
}
String toJson() => json.encode(toMap());
factory SignUp.fromJson(String source) =>
SignUp.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() => 'SignUp(sessionId: $sessionId, password: $password)';
}
class TokenSuccess {
final String accessToken;
final String refreshToken;
final Account account;
TokenSuccess({
required this.accessToken,
required this.refreshToken,
required this.account,
});
TokenSuccess copyWith({
String? accessToken,
String? refreshToken,
Account? account,
}) {
return TokenSuccess(
accessToken: accessToken ?? this.accessToken,
refreshToken: refreshToken ?? this.refreshToken,
account: account ?? this.account,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'access_token': accessToken,
'refresh_token': refreshToken,
'account': account.toMap(),
};
}
factory TokenSuccess.fromMap(Map<String, dynamic> map) {
return TokenSuccess(
accessToken: map['access_token'] as String,
refreshToken: map['refresh_token'] as String,
account: Account.fromMap(map['account'] as Map<String, dynamic>),
);
}
String toJson() => json.encode(toMap());
factory TokenSuccess.fromJson(String source) =>
TokenSuccess.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() =>
'TokenSuccess(accessToken: $accessToken, refreshToken: $refreshToken, account: $account)';
}
class Login {
final String email;
final String password;
Login({
required this.email,
required this.password,
});
Login copyWith({
String? email,
String? password,
}) {
return Login(
email: email ?? this.email,
password: password ?? this.password,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'email': email,
'password': password,
};
}
factory Login.fromMap(Map<String, dynamic> map) {
return Login(
email: map['email'] as String,
password: map['password'] as String,
);
}
String toJson() => json.encode(toMap());
factory Login.fromJson(String source) =>
Login.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() => 'Login(email: $email, password: $password)';
}
class FastAPI {
final String baseUrl;
final MiddlewareClient client;
final int apiVersion;
FastAPI({
this.baseUrl = 'localhost:80',
MiddlewareClient? client,
this.apiVersion = 1,
}) : client = client ?? MiddlewareClient();
String get apiPath => '/api/v$apiVersion';
Future<void> sendSignUpCode(String email) async {
final r = await client.post(
Uri.parse('$apiPath/auth/send-sign-up-code'),
body: <String, String>{
'email': email,
},
);
if (r.statusCode != 201) {
throw Exception('Invalid reponse: ${r.statusCode}');
}
}
Future<Account> verifyCode(VerifyCode verifyCode) async {
final r = await client.post(
Uri.parse('$apiPath/auth/verify-code'),
body: verifyCode.toMap(),
);
if (r.statusCode != 202) {
throw Exception('Invalid reponse: ${r.statusCode}');
} else {
return Account.fromMap(
(jsonDecode(r.body) as Map<String, dynamic>)['account']
as Map<String, dynamic>,
);
}
}
Future<Account> signUp(SignUp signUp) async {
final r = await client.post(
Uri.parse('$apiPath/auth/sign-up'),
body: signUp.toMap(),
);
if (r.statusCode != 201) {
throw Exception('Invalid reponse: ${r.statusCode}');
} else {
return Account.fromJson(r.body);
}
}
Future<TokenSuccess> signInWithPassword(Login login) async {
final r = await client.post(
Uri.parse('$apiPath/auth/sign-in-with-password'),
body: login.toMap(),
);
if (r.statusCode != 200) {
throw Exception('Invalid reponse: ${r.statusCode}');
} else {
return TokenSuccess.fromJson(r.body);
}
}
// Future<TokenSuccess> refresh() async {
// final r = await client.refresh();
// return TokenSuccess.fromJson(r?.body ?? '');
// }
Future<List<Account>> getAccountList() async {
final r = await client.get(
Uri.parse('$apiPath/account'),
);
if (r.statusCode != 200) {
throw Exception('Invalid reponse: ${r.statusCode}');
} else {
final list = (jsonDecode(r.body) as Map<String, dynamic>)['founds']
as List<Map<String, dynamic>>;
final result = <Account>[];
for (final element in list) {
result.add(Account.fromMap(element));
}
return result;
}
}
}
void main(List<String> args) async {
final Pipeline pipeline = Pipeline()
.addMiddleware(
UriPrefixMiddleware(
protocol: Protocols.http,
authority: 'localhost:80',
),
)
.addMiddleware(BodyToJsonMiddleware())
.addMiddleware(
RefreshTokenAuthMiddleware(
authorizationEndpoint: '/api/v1/auth/sign-in-with-password',
tokenEndpoint: '/api/v1/auth/refresh',
accessTokenParser: (body) => body['access_token']! as String,
refreshTokenParser: (body) => body['refresh_token']! as String,
unauthorized: HttpStatus.forbidden,
),
)
.addMiddleware(SimpleLoggerMiddleware());
print(pipeline);
final client = MiddlewareClient(pipeline: pipeline);
final api = FastAPI(
client: client,
);
await api.sendSignUpCode('git@pcl.ovh');
final verifiedAccount = await api.verifyCode(
VerifyCode(
email: 'git@pcl.ovh',
verificationCode: '000000000',
action: EmailVerificationAction.signUp,
),
);
final registeredAccount = await api.signUp(
SignUp(sessionId: verifiedAccount.sessionId ?? '', password: 'password'),
);
final signedInAccount = await api.signInWithPassword(
Login(email: 'git@pcl.ovh', password: 'password'),
);
final accountList = await api.getAccountList();
print(accountList);
}
@@ -0,0 +1,162 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_http_client/src/middleware_client.dart';
import 'package:wyatt_http_client/src/middlewares/body_to_json_middleware.dart';
import 'package:wyatt_http_client/src/middlewares/simple_logger_middleware.dart';
import 'package:wyatt_http_client/src/middlewares/unsafe_auth_middleware.dart';
import 'package:wyatt_http_client/src/middlewares/uri_prefix_middleware.dart';
import 'package:wyatt_http_client/src/pipeline.dart';
import 'package:wyatt_http_client/src/utils/protocols.dart';
// class RequestMutatorMiddleware implements Middleware {
// @override
// Middleware? parent;
// @override
// Middleware? child;
// RequestMutatorMiddleware({
// this.parent,
// this.child,
// });
// @override
// BaseRequest onRequest(BaseRequest request) {
// print('RequestMutator::OnRequest: ${request.method} -> ${request.url}');
// return child?.onRequest(request) ?? request;
// }
// @override
// BaseResponse onResponse(BaseResponse response) {
// final res = child?.onResponse(response) ?? response;
// print(
// 'RequestMutator::OnResponse: ${res.statusCode} -> ${res.contentLength} bytes',
// );
// return res;
// }
// }
// typedef Middleware = Handler Function(Handler innerHandler);
// Middleware createMiddleware({
// FutureOr<Response?> Function(Request)? requestHandler,
// FutureOr<Response> Function(Response)? responseHandler,
// FutureOr<Response> Function(Object error, StackTrace)? errorHandler,
// }) {
// requestHandler ??= (request) => null;
// responseHandler ??= (response) => response;
// FutureOr<Response> Function(Object, StackTrace)? onError;
// if (errorHandler != null) {
// onError = (error, stackTrace) {
// if (error is Exception) throw error;
// return errorHandler(error, stackTrace);
// };
// }
// return (Handler innerHandler) {
// return (request) {
// return Future.sync(() => requestHandler!(request)).then((response) {
// if (response != null) return response;
// return Future.sync(() => innerHandler(request))
// .then((response) => responseHandler!(response), onError: onError);
// });
// };
// };
// }
// extension MiddlewareX on Middleware {
// Middleware addMiddleware(Middleware other) =>
// (Handler handler) => this(other(handler));
// Handler addHandler(Handler handler) => this(handler);
// }
// typedef Handler = FutureOr<Response> Function(Request request);
// final headerMutator = createMiddleware(
// responseHandler: (response) {
// print(response.headers);
// return response;
// },);
// class Pipeline {
// const Pipeline();
// Pipeline addMiddleware(Middleware middleware) =>
// _Pipeline(middleware, addHandler);
// Handler addHandler(Handler handler) => handler;
// Middleware get middleware => addHandler;
// }
// class _Pipeline extends Pipeline {
// final Middleware _middleware;
// final Middleware _parent;
// _Pipeline(this._middleware, this._parent);
// @override
// Handler addHandler(Handler handler) => _parent(_middleware(handler));
// }
Future<void> main(List<String> args) async {
final UnsafeAuthMiddleware auth = UnsafeAuthMiddleware();
final Pipeline pipeline = Pipeline()
.addMiddleware(
UriPrefixMiddleware(
protocol: Protocols.http,
authority: 'localhost:80',
),
)
.addMiddleware(BodyToJsonMiddleware())
.addMiddleware(
UnsafeAuthMiddleware(
username: 'wyatt',
password: 'motdepasse',
),
)
.addMiddleware(SimpleLoggerMiddleware());
// .addMiddleware(
// RefreshTokenMiddleware(
// authorizationEndpoint: '/api/v1/account/test?action=authorize',
// tokenEndpoint: '/api/v1/account/test?action=refresh',
// accessTokenParser: (body) => body['access_token']! as String,
// refreshTokenParser: (body) => body['refresh_token']! as String,
// ),
// );
print(pipeline);
final client = MiddlewareClient(pipeline: pipeline);
await client.post(
Uri.parse('/api/v1/account/test'),
body: <String, String>{
'email': 'test@test.fr',
},
);
auth
..username = 'username'
..password = 'password';
await client.post(
Uri.parse('/api/v1/account/test'),
body: <String, String>{
'email': 'test@test.fr',
},
);
}
@@ -1,67 +0,0 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:convert';
import 'package:http/http.dart';
import 'package:wyatt_http_client/src/authentication/interfaces/header_authentication_client.dart';
import 'package:wyatt_http_client/src/utils/authentication_methods.dart';
import 'package:wyatt_http_client/src/utils/header_keys.dart';
import 'package:wyatt_http_client/src/utils/http_status.dart';
import 'package:wyatt_http_client/src/utils/utils.dart';
class BasicAuthenticationClient extends HeaderAuthenticationClient {
final String username;
final String password;
final bool preemptive;
final String authenticationHeader;
BasicAuthenticationClient({
required this.username,
required this.password,
this.preemptive = true,
this.authenticationHeader = HeaderKeys.authorization,
BaseClient? inner,
}) : super(inner);
@override
Map<String, String> modifyHeader(
Map<String, String> header, [
BaseRequest? request,
]) {
header[authenticationHeader] = '${AuthenticationMethods.basic} '
'${base64Encode(utf8.encode('$username:$password'))}';
return header;
}
@override
Future<StreamedResponse> send(BaseRequest request) async {
if (preemptive) {
// Just send request with modified header.
return super.send(request);
}
// Try to send request without modified header,
// and if it fails, send it with.
final response = await inner.send(request);
if (response.statusCode == HttpStatus.unauthorized) {
// TODO(hpcl): save realm.
final newRequest = Utils.copyRequest(request);
return super.send(newRequest);
} else {
return response;
}
}
}
@@ -1,63 +0,0 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:http/http.dart';
import 'package:wyatt_http_client/src/authentication/interfaces/header_authentication_client.dart';
import 'package:wyatt_http_client/src/utils/authentication_methods.dart';
import 'package:wyatt_http_client/src/utils/header_keys.dart';
import 'package:wyatt_http_client/src/utils/http_status.dart';
import 'package:wyatt_http_client/src/utils/utils.dart';
class BearerAuthenticationClient extends HeaderAuthenticationClient {
final String token;
final bool preemptive;
final String authenticationHeader;
final String authenticationMethod;
BearerAuthenticationClient({
required this.token,
this.preemptive = true,
this.authenticationHeader = HeaderKeys.authorization,
this.authenticationMethod = AuthenticationMethods.bearer,
BaseClient? inner,
}) : super(inner);
@override
Map<String, String> modifyHeader(
Map<String, String> header, [
BaseRequest? request,
]) {
header[authenticationHeader] = '$authenticationMethod $token';
return header;
}
@override
Future<StreamedResponse> send(BaseRequest request) async {
if (preemptive) {
// Just send request with modified header.
return super.send(request);
}
// Try to send request without modified header,
final response = await inner.send(request);
if (response.statusCode == HttpStatus.unauthorized) {
final newRequest = Utils.copyRequest(request);
return super.send(newRequest);
} else {
return response;
}
}
}
@@ -1,75 +0,0 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:http/http.dart';
import 'package:wyatt_http_client/src/authentication/interfaces/header_authentication_client.dart';
import 'package:wyatt_http_client/src/utils/digest_auth.dart';
import 'package:wyatt_http_client/src/utils/header_keys.dart';
import 'package:wyatt_http_client/src/utils/http_status.dart';
import 'package:wyatt_http_client/src/utils/utils.dart';
class DigestAuthenticationClient extends HeaderAuthenticationClient {
final String username;
final String password;
final DigestAuth _digestAuth;
final String authenticationHeader;
final String wwwAuthenticateHeader;
DigestAuthenticationClient({
required this.username,
required this.password,
this.authenticationHeader = HeaderKeys.authorization,
this.wwwAuthenticateHeader = HeaderKeys.wwwAuthenticate,
BaseClient? inner,
}) : _digestAuth = DigestAuth(username, password),
super(inner);
@override
Map<String, String> modifyHeader(
Map<String, String> header, [
BaseRequest? request,
]) {
if ((_digestAuth.isReady()) && request != null) {
header[authenticationHeader] = _digestAuth.getAuthString(
request.method,
request.url,
);
}
return header;
}
@override
Future<StreamedResponse> send(BaseRequest request) async {
// Check if our DigestAuth is ready.
if (_digestAuth.isReady()) {
// If it is, try to send the request with the modified header.
return super.send(request);
}
// If it isn't, try to send the request without the modified header.
final response = await inner.send(request);
if (response.statusCode == HttpStatus.unauthorized) {
final newRequest = Utils.copyRequest(request);
final authInfo =
response.headers[HeaderKeys.wwwAuthenticate.toLowerCase()];
_digestAuth.initFromAuthenticateHeader(authInfo);
return super.send(newRequest);
} else {
return response;
}
}
}
@@ -1,36 +0,0 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:http/http.dart';
import 'package:wyatt_http_client/src/authentication/interfaces/authentication_client.dart';
abstract class HeaderAuthenticationClient extends AuthenticationClient {
HeaderAuthenticationClient(super.inner);
Map<String, String> modifyHeader(
Map<String, String> header, [
BaseRequest? request,
]) =>
header;
@override
Future<StreamedResponse> send(BaseRequest request) {
final newHeader = modifyHeader(Map.from(request.headers), request);
request.headers.clear();
request.headers.addAll(newHeader);
return super.send(request);
}
}
@@ -1,115 +0,0 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:convert';
import 'package:http/http.dart';
import 'package:wyatt_http_client/src/authentication/interfaces/oauth2_client.dart';
import 'package:wyatt_http_client/src/utils/authentication_methods.dart';
import 'package:wyatt_http_client/src/utils/header_keys.dart';
import 'package:wyatt_http_client/src/utils/http_status.dart';
import 'package:wyatt_http_client/src/utils/utils.dart';
class RefreshTokenClient extends Oauth2Client {
final String authorizationEndpoint;
final String tokenEndpoint;
String? accessToken;
final TokenParser accessTokenParser;
String? refreshToken;
final TokenParser refreshTokenParser;
final String authenticationHeader;
final String authenticationMethod;
RefreshTokenClient({
required this.authorizationEndpoint,
required this.tokenEndpoint,
required this.accessTokenParser,
required this.refreshTokenParser,
this.authenticationHeader = HeaderKeys.authorization,
this.authenticationMethod = AuthenticationMethods.bearer,
BaseClient? inner,
}) : super(inner);
@override
Map<String, String> modifyHeader(
Map<String, String> header, [
BaseRequest? request,
]) {
if (accessToken != null && request != null) {
header[authenticationHeader] = '$authenticationMethod $accessToken';
return header;
}
return header;
}
@override
Future<void> authorize(
Map<String, dynamic> body, {
Map<String, String>? headers,
}) async {
final response = await inner.post(
Uri.parse(authorizationEndpoint),
body: jsonEncode(body),
headers: headers,
);
if (response.statusCode == HttpStatus.ok) {
final body = json.decode(response.body) as Map<String, dynamic>;
final accessToken = accessTokenParser(body);
final refreshToken = refreshTokenParser(body);
if (accessToken.isNotEmpty) {
this.accessToken = accessToken;
}
if (refreshToken.isNotEmpty) {
this.refreshToken = refreshToken;
}
}
}
@override
Future<void> refresh() async {
if (refreshToken != null) {
final Map<String, String> header = {
authenticationHeader: '$authenticationHeader $refreshToken',
};
final response = await inner.get(
Uri.parse(tokenEndpoint),
headers: header,
);
if (response.statusCode == HttpStatus.ok) {
final body = json.decode(response.body) as Map<String, dynamic>;
accessToken = accessTokenParser(body);
}
}
}
@override
Future<StreamedResponse> send(BaseRequest request) async {
final response = await super.send(request);
if (response.statusCode == HttpStatus.unauthorized) {
await refresh();
return super.send(Utils.copyRequest(request));
}
return response;
}
}
@@ -0,0 +1,38 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_http_client/src/models/middleware_context.dart';
import 'package:wyatt_http_client/src/models/middleware_request.dart';
import 'package:wyatt_http_client/src/models/middleware_response.dart';
abstract class Middleware {
Middleware();
String getName();
}
mixin OnRequestMiddleware {
Future<MiddlewareRequest> onRequest(
MiddlewareContext context,
MiddlewareRequest request,
);
}
mixin OnResponseMiddleware {
Future<MiddlewareResponse> onResponse(
MiddlewareContext context,
MiddlewareResponse response,
);
}
@@ -0,0 +1,129 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:convert';
import 'package:http/http.dart';
import 'package:wyatt_http_client/src/models/middleware_context.dart';
import 'package:wyatt_http_client/src/models/middleware_request.dart';
import 'package:wyatt_http_client/src/models/middleware_response.dart';
import 'package:wyatt_http_client/src/models/unfreezed_request.dart';
import 'package:wyatt_http_client/src/pipeline.dart';
import 'package:wyatt_http_client/src/utils/http_methods.dart';
class MiddlewareClient extends BaseClient {
final Client inner;
final Pipeline pipeline;
MiddlewareClient({
Pipeline? pipeline,
Client? inner,
}) : pipeline = pipeline ?? Pipeline(),
inner = inner ?? Client() {
print('Using Pipeline:\n$pipeline');
}
@override
Future<Response> head(Uri url, {Map<String, String>? headers}) =>
_sendUnstreamed(HttpMethods.head.method, url, headers);
@override
Future<Response> get(Uri url, {Map<String, String>? headers}) =>
_sendUnstreamed(HttpMethods.get.method, url, headers);
@override
Future<Response> post(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) =>
_sendUnstreamed(HttpMethods.post.method, url, headers, body, encoding);
@override
Future<Response> put(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) =>
_sendUnstreamed(HttpMethods.put.method, url, headers, body, encoding);
@override
Future<Response> patch(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) =>
_sendUnstreamed(HttpMethods.patch.method, url, headers, body, encoding);
@override
Future<Response> delete(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) =>
_sendUnstreamed(HttpMethods.delete.method, url, headers, body, encoding);
@override
Future<StreamedResponse> send(BaseRequest request) {
return inner.send(request);
}
Future<Response> _sendUnstreamed(
String method,
Uri url,
Map<String, String>? headers, [
Object? body,
Encoding? encoding,
]) async {
final originalRequest = MiddlewareRequest(
unfreezedRequest: UnfreezedRequest(
method: method,
url: url,
headers: headers,
body: body,
encoding: encoding,
),
);
final requestContext = MiddlewareContext(
pipeline: pipeline,
client: this,
originalRequest: originalRequest,
);
final modifiedRequest = await pipeline.onRequest(
requestContext,
originalRequest.copyWith(),
);
final originalResponse = MiddlewareResponse(
httpResponse: await Response.fromStream(
await send(modifiedRequest.request),
),
);
final responseContext =
requestContext.copyWith(originalResponse: originalResponse);
final modifiedResponse =
await pipeline.onResponse(responseContext, originalResponse.copyWith());
return modifiedResponse.httpResponse as Response;
}
}
@@ -0,0 +1,16 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
@@ -0,0 +1,59 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:convert';
import 'package:wyatt_http_client/src/middleware.dart';
import 'package:wyatt_http_client/src/models/middleware_context.dart';
import 'package:wyatt_http_client/src/models/middleware_request.dart';
import 'package:wyatt_http_client/src/utils/authentication_methods.dart';
import 'package:wyatt_http_client/src/utils/header_keys.dart';
class BasicAuthMiddleware with OnRequestMiddleware implements Middleware {
String? username;
String? password;
final String authenticationHeader;
BasicAuthMiddleware({
this.username,
this.password,
this.authenticationHeader = HeaderKeys.authorization,
});
@override
String getName() => 'BasicAuth';
@override
Future<MiddlewareRequest> onRequest(
MiddlewareContext context,
MiddlewareRequest request,
) async {
if (username == null || password == null) {
return request;
}
print(
'${getName()}::OnRequest\n'
'>> Basic: ${base64Encode(utf8.encode('$username:$password'))}',
);
final mutation = {
authenticationHeader: '${AuthenticationMethods.basic} '
'${base64Encode(utf8.encode('$username:$password'))}',
};
final Map<String, String> headers = request.headers..addAll(mutation);
request.modifyRequest(request.unfreezedRequest.copyWith(headers: headers));
return request;
}
}
@@ -0,0 +1,49 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:convert';
import 'package:wyatt_http_client/src/middleware.dart';
import 'package:wyatt_http_client/src/models/middleware_context.dart';
import 'package:wyatt_http_client/src/models/middleware_request.dart';
class BodyToJsonMiddleware with OnRequestMiddleware implements Middleware {
@override
String getName() => 'BodyToJson';
@override
Future<MiddlewareRequest> onRequest(
MiddlewareContext context,
MiddlewareRequest request,
) async {
print(
'${getName()}::OnRequest\n'
'>> Transforms body in json if Map then update '
'headers with right content-type',
);
final mutation = {
'content-type': 'application/json; charset=utf-8',
};
if (request.body is Map) {
final Map<String, String> headers = request.headers..addAll(mutation);
request.modifyRequest(
request.unfreezedRequest
.copyWith(headers: headers, body: jsonEncode(request.body)),
);
}
return request;
}
}
@@ -0,0 +1,22 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_http_client/src/middleware.dart';
class DefaultMiddleware implements Middleware {
@override
String getName() => 'DefaultMiddleware';
}
@@ -0,0 +1,92 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_http_client/src/middleware.dart';
import 'package:wyatt_http_client/src/models/middleware_context.dart';
import 'package:wyatt_http_client/src/models/middleware_request.dart';
import 'package:wyatt_http_client/src/models/middleware_response.dart';
import 'package:wyatt_http_client/src/utils/digest_auth.dart';
import 'package:wyatt_http_client/src/utils/header_keys.dart';
import 'package:wyatt_http_client/src/utils/http_status.dart';
class DigestAuthMiddleware
with OnRequestMiddleware, OnResponseMiddleware
implements Middleware {
final String username;
final String password;
final DigestAuth _digestAuth;
final String authenticationHeader;
final String wwwAuthenticateHeader;
final HttpStatus unauthorized;
DigestAuthMiddleware({
required this.username,
required this.password,
this.authenticationHeader = HeaderKeys.authorization,
this.wwwAuthenticateHeader = HeaderKeys.wwwAuthenticate,
this.unauthorized = HttpStatus.unauthorized,
}) : _digestAuth = DigestAuth(username, password);
@override
String getName() => 'DigestAuth';
@override
Future<MiddlewareRequest> onRequest(
MiddlewareContext context,
MiddlewareRequest request,
) async {
print(
'${getName()}::OnRequest\n'
'>> Digest ready: ${_digestAuth.isReady()}',
);
if (_digestAuth.isReady()) {
final mutation = {
authenticationHeader: _digestAuth.getAuthString(
request.method,
request.url,
),
};
final Map<String, String> headers = request.headers..addAll(mutation);
request
.modifyRequest(request.unfreezedRequest.copyWith(headers: headers));
}
return request;
}
@override
Future<MiddlewareResponse> onResponse(
MiddlewareContext context,
MiddlewareResponse response,
) async {
if (response.status == unauthorized) {
final authInfo =
response.headers[HeaderKeys.wwwAuthenticate.toLowerCase()];
_digestAuth.initFromAuthenticateHeader(authInfo);
final MiddlewareRequest? newRequest = context.lastRequest?.copyWith();
if (newRequest != null) {
final newResponse = await context.client.send(newRequest.request);
return MiddlewareResponse(httpResponse: newResponse);
}
}
print(
'${getName()}::OnResponse\n'
'>> Digest ready: ${_digestAuth.isReady()}',
);
return response;
}
}
@@ -0,0 +1,25 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'access_token_auth_middleware.dart';
export 'basic_auth_middleware.dart';
export 'body_to_json_middleware.dart';
export 'default_middleware.dart';
export 'digest_auth_middleware.dart';
export 'refresh_token_auth_middleware.dart';
export 'simple_logger_middleware.dart';
export 'unsafe_auth_middleware.dart';
export 'uri_prefix_middleware.dart';
@@ -0,0 +1,191 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:convert';
import 'package:wyatt_http_client/src/middleware.dart';
import 'package:wyatt_http_client/src/middleware_client.dart';
import 'package:wyatt_http_client/src/models/middleware_context.dart';
import 'package:wyatt_http_client/src/models/middleware_request.dart';
import 'package:wyatt_http_client/src/models/middleware_response.dart';
import 'package:wyatt_http_client/src/utils/authentication_methods.dart';
import 'package:wyatt_http_client/src/utils/delay.dart';
import 'package:wyatt_http_client/src/utils/header_keys.dart';
import 'package:wyatt_http_client/src/utils/http_status.dart';
typedef TokenParser = String Function(Map<String, dynamic>);
class RefreshTokenAuthMiddleware
with OnRequestMiddleware, OnResponseMiddleware
implements Middleware {
final String authorizationEndpoint;
final String tokenEndpoint;
String? accessToken;
final TokenParser accessTokenParser;
String? refreshToken;
final TokenParser refreshTokenParser;
final String authenticationHeader;
final String authenticationMethod;
final HttpStatus unauthorized;
final int maxAttempts;
RefreshTokenAuthMiddleware({
required this.authorizationEndpoint,
required this.tokenEndpoint,
required this.accessTokenParser,
required this.refreshTokenParser,
this.authenticationHeader = HeaderKeys.authorization,
this.authenticationMethod = AuthenticationMethods.bearer,
this.unauthorized = HttpStatus.unauthorized,
this.maxAttempts = 8,
});
@override
String getName() => 'RefreshToken';
Future<MiddlewareRequest?> refresh(MiddlewareContext context) async {
final subPipeline = context.pipeline.sub(this);
final httpClient = MiddlewareClient(
pipeline: subPipeline,
inner: context.client.inner,
);
final headers = {
authenticationHeader: '$authenticationMethod $refreshToken',
};
final response = MiddlewareResponse(
httpResponse: await httpClient.get(
Uri.parse(tokenEndpoint),
headers: headers,
),
);
if (response.status.isSuccess()) {
final body = jsonDecode(response.body) as Map<String, dynamic>;
accessToken = accessTokenParser(body);
// Then modify current request with accessToken
final mutation = {
authenticationHeader: '$authenticationMethod $accessToken',
};
final Map<String, String>? headers = context.lastRequest?.headers
?..addAll(mutation);
final newRequest = context.lastRequest?.copyWith(
unfreezedRequest:
context.lastRequest?.unfreezedRequest.copyWith(headers: headers),
);
return newRequest;
}
return null;
}
Future<MiddlewareRequest?> retry(MiddlewareContext context) async {
// Retry
int attempt = 1;
while (attempt <= maxAttempts) {
// Delayed before retry
await Future<void>.delayed(Delay.getRetryDelay(attempt));
final newRequest = await refresh(context);
if (newRequest != null) {
return newRequest;
}
attempt++;
}
return null;
}
@override
Future<MiddlewareRequest> onRequest(
MiddlewareContext context,
MiddlewareRequest request,
) async {
print(
'${getName()}::OnRequest\n'
'>> accessToken: $accessToken\n'
'>> refreshToken: $refreshToken',
);
// Check if it is authorization
if (context.originalRequest?.url == Uri.parse(authorizationEndpoint)) {
return request;
}
// Check if it is refresh
if (context.originalRequest?.url == Uri.parse(tokenEndpoint)) {
return request;
}
// If AccessToken not null then return request with authorization header
if (accessToken != null) {
final mutation = {
authenticationHeader: '$authenticationMethod $accessToken',
};
final Map<String, String> headers = request.headers..addAll(mutation);
request
.modifyRequest(request.unfreezedRequest.copyWith(headers: headers));
return request;
}
// If AccessToken is null BUT there is a refreshToken, then try refreshing
if (refreshToken != null) {
MiddlewareRequest? newRequest = await refresh(context);
newRequest ??= await retry(context);
return newRequest ?? request;
}
// Pass
return request;
}
@override
Future<MiddlewareResponse> onResponse(
MiddlewareContext context,
MiddlewareResponse response,
) async {
// Check if it is authorization
if (context.originalRequest?.url == Uri.parse(authorizationEndpoint)) {
// If success, then update tokens
if (response.status.isSuccess()) {
final body = jsonDecode(response.body) as Map<String, dynamic>;
final accessToken = accessTokenParser(body);
final refreshToken = refreshTokenParser(body);
if (accessToken.isNotEmpty) {
this.accessToken = accessToken;
}
if (refreshToken.isNotEmpty) {
this.refreshToken = refreshToken;
}
}
}
print(
'${getName()}::OnResponse\n'
'>> accessToken: $accessToken\n'
'>> refreshToken: $refreshToken',
);
if (response.status == unauthorized) {
// Refresh
MiddlewareRequest? newRequest = await refresh(context);
newRequest ??= await retry(context);
if (newRequest != null) {
return response.copyWith(
httpResponse: await context.client.send(newRequest.request),
);
}
}
return response;
}
}
@@ -0,0 +1,54 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_http_client/src/middleware.dart';
import 'package:wyatt_http_client/src/models/middleware_context.dart';
import 'package:wyatt_http_client/src/models/middleware_request.dart';
import 'package:wyatt_http_client/src/models/middleware_response.dart';
class SimpleLoggerMiddleware
with OnRequestMiddleware, OnResponseMiddleware
implements Middleware {
@override
String getName() => 'SimpleLogger';
@override
Future<MiddlewareRequest> onRequest(
MiddlewareContext context,
MiddlewareRequest request,
) async {
print(
'${getName()}::OnRequest\n'
'>> ${request.method} ${request.url}\n'
'>> Headers: ${request.headers}\n>> Body: ${request.encodedBody}',
);
return request;
}
@override
Future<MiddlewareResponse> onResponse(
MiddlewareContext context,
MiddlewareResponse response,
) async {
print(
'${getName()}::OnResponse\n'
'>> Status: ${response.status.name.toUpperCase()}\n'
'>> Length: ${response.contentLength ?? '0'} bytes',
// '>> Body: ${response.body}',
);
return response;
}
}

Some files were not shown because too many files have changed in this diff Show More