From f4240d1c69c161b551ed3b313108932ebda3c14a Mon Sep 17 00:00:00 2001 From: Tim Shedor Date: Mon, 16 Dec 2024 11:23:09 -0800 Subject: [PATCH] eng(rest): migrate RestProvider to use query v2 (#498) --- .../brick_core/test/query/query_test.dart | 276 ++++++++------- packages/brick_rest/analysis_options.yaml | 325 ++++++++++++++++++ packages/brick_rest/lib/brick_rest.dart | 1 + packages/brick_rest/lib/gzip_http_client.dart | 2 + packages/brick_rest/lib/rest_exception.dart | 4 + .../brick_rest/lib/src/annotations/rest.dart | 2 +- packages/brick_rest/lib/src/rest_adapter.dart | 4 +- packages/brick_rest/lib/src/rest_model.dart | 2 +- .../lib/src/rest_model_dictionary.dart | 1 + .../brick_rest/lib/src/rest_provider.dart | 74 ++-- .../lib/src/rest_provider_query.dart | 27 ++ packages/brick_rest/lib/src/rest_request.dart | 57 ++- .../lib/src/rest_request_transformer.dart | 15 + packages/brick_rest/test/__mocks__.dart | 35 +- .../test/rest_provider_query_test.dart | 141 ++++++++ .../brick_rest/test/rest_provider_test.dart | 62 +++- .../analysis_options.yaml | 325 ++++++++++++++++++ .../lib/rest_model_serdes_generator.dart | 4 +- .../lib/src/rest_deserialize.dart | 1 + .../lib/src/rest_fields.dart | 7 +- .../lib/src/rest_serdes_generator.dart | 2 + .../lib/src/rest_serializable_extended.dart | 2 + .../lib/src/rest_serialize.dart | 1 + packages/brick_rest_generators/pubspec.yaml | 4 +- ...est_constructor_member_field_mismatch.dart | 2 +- .../test_enum_as_string.dart | 2 +- .../test_ignore_from_to.dart | 2 +- ...t_unserializable_field_with_generator.dart | 2 +- .../rest_model_serdes_generator_test.dart | 2 +- 29 files changed, 1158 insertions(+), 226 deletions(-) create mode 100644 packages/brick_rest/lib/src/rest_provider_query.dart create mode 100644 packages/brick_rest/test/rest_provider_query_test.dart diff --git a/packages/brick_core/test/query/query_test.dart b/packages/brick_core/test/query/query_test.dart index 8c1fdb2a..80ade070 100644 --- a/packages/brick_core/test/query/query_test.dart +++ b/packages/brick_core/test/query/query_test.dart @@ -6,178 +6,174 @@ import 'package:test/test.dart'; void main() { group('Query', () { - group('properties', () { - test('#action', () { - const q = Query(action: QueryAction.delete); - expect(q.action, QueryAction.delete); - }); + test('#action', () { + const q = Query(action: QueryAction.delete); + expect(q.action, QueryAction.delete); + }); - group('#providerArgs', () { - test('#providerArgs.page and #providerArgs.sort', () { - const q = Query(providerArgs: {'page': 1, 'sort': 'by_user_asc'}); + group('#providerArgs', () { + test('#providerArgs.page and #providerArgs.sort', () { + const q = Query(providerArgs: {'page': 1, 'sort': 'by_user_asc'}); - expect(q.providerArgs['page'], 1); - expect(q.providerArgs['sort'], 'by_user_asc'); - }); + expect(q.providerArgs['page'], 1); + expect(q.providerArgs['sort'], 'by_user_asc'); + }); + }); - test('#providerArgs.limit', () { - const q0 = Query(limit: 0); - expect(q0.limit, 0); + test('#limit', () { + const q0 = Query(limit: 0); + expect(q0.limit, 0); - const q10 = Query(limit: 10); - expect(q10.limit, 10); + const q10 = Query(limit: 10); + expect(q10.limit, 10); - const q18 = Query(limit: 18); - expect(q18.limit, 18); - }); + const q18 = Query(limit: 18); + expect(q18.limit, 18); + }); - test('#providerArgs.offset', () { - const q0 = Query(limit: 10, offset: 0); - expect(q0.offset, 0); + test('#offset', () { + const q0 = Query(limit: 10, offset: 0); + expect(q0.offset, 0); - const q10 = Query(limit: 10, offset: 10); - expect(q10.offset, 10); + const q10 = Query(limit: 10, offset: 10); + expect(q10.offset, 10); - const q18 = Query(limit: 10, offset: 18); - expect(q18.offset, 18); - }); - }); + const q18 = Query(limit: 10, offset: 18); + expect(q18.offset, 18); + }); - test('#where', () { - const q = Query( - where: [ - Where('name', value: 'Thomas'), - ], - ); + test('#where', () { + const q = Query( + where: [ + Where('name', value: 'Thomas'), + ], + ); - expect(q.where!.first.evaluatedField, 'name'); - expect(q.where!.first.value, 'Thomas'); - }); + expect(q.where!.first.evaluatedField, 'name'); + expect(q.where!.first.value, 'Thomas'); }); + }); - group('==', () { - test('properties are the same', () { - const q1 = Query( - action: QueryAction.delete, - limit: 3, - offset: 3, - ); - const q2 = Query( - action: QueryAction.delete, - limit: 3, - offset: 3, - ); - - expect(q1, q2); - }); + group('==', () { + test('properties are the same', () { + const q1 = Query( + action: QueryAction.delete, + limit: 3, + offset: 3, + ); + const q2 = Query( + action: QueryAction.delete, + limit: 3, + offset: 3, + ); - test('providerArgs are the same', () { - const q1 = Query(providerArgs: {'name': 'Guy'}); - const q2 = Query(providerArgs: {'name': 'Guy'}); + expect(q1, q2); + }); - expect(q1, q2); - }); + test('providerArgs are the same', () { + const q1 = Query(providerArgs: {'name': 'Guy'}); + const q2 = Query(providerArgs: {'name': 'Guy'}); - test('providerArgs have different values', () { - const q1 = Query(providerArgs: {'name': 'Thomas'}); - const q2 = Query(providerArgs: {'name': 'Guy'}); + expect(q1, q2); + }); - expect(q1, isNot(q2)); - }); + test('providerArgs have different values', () { + const q1 = Query(providerArgs: {'name': 'Thomas'}); + const q2 = Query(providerArgs: {'name': 'Guy'}); - test('providerArgs have different keys', () { - const q1 = Query(providerArgs: {'email': 'guy@guy.com'}); - const q2 = Query(providerArgs: {'name': 'Guy'}); + expect(q1, isNot(q2)); + }); - expect(q1, isNot(q2)); - }); + test('providerArgs have different keys', () { + const q1 = Query(providerArgs: {'email': 'guy@guy.com'}); + const q2 = Query(providerArgs: {'name': 'Guy'}); - test('providerArgs are null', () { - const q1 = Query(); - const q2 = Query(providerArgs: {'name': 'Guy'}); - expect(q1, isNot(q2)); + expect(q1, isNot(q2)); + }); - const q3 = Query(); - expect(q1, q3); - }); + test('providerArgs are null', () { + const q1 = Query(); + const q2 = Query(providerArgs: {'name': 'Guy'}); + expect(q1, isNot(q2)); + + const q3 = Query(); + expect(q1, q3); }); + }); - group('#copyWith', () { - test('overrides', () { - const q1 = Query(action: QueryAction.insert, limit: 10); - final q2 = q1.copyWith(limit: 20); - expect(q2.action, QueryAction.insert); - expect(q2.limit, 20); - expect(q2.offset, null); - - final q3 = q1.copyWith(limit: 50, offset: 20); - expect(q3.action, QueryAction.insert); - expect(q3.limit, 50); - expect(q3.offset, 20); - }); + group('#copyWith', () { + test('overrides', () { + const q1 = Query(action: QueryAction.insert, limit: 10); + final q2 = q1.copyWith(limit: 20); + expect(q2.action, QueryAction.insert); + expect(q2.limit, 20); + expect(q2.offset, null); + + final q3 = q1.copyWith(limit: 50, offset: 20); + expect(q3.action, QueryAction.insert); + expect(q3.limit, 50); + expect(q3.offset, 20); + }); - test('appends', () { - const q1 = Query(action: QueryAction.insert); - final q2 = q1.copyWith(limit: 20); + test('appends', () { + const q1 = Query(action: QueryAction.insert); + final q2 = q1.copyWith(limit: 20); - expect(q1.limit, null); - expect(q2.action, QueryAction.insert); - expect(q2.limit, 20); - }); + expect(q1.limit, null); + expect(q2.action, QueryAction.insert); + expect(q2.limit, 20); }); + }); + + test('#toJson', () { + const source = Query( + action: QueryAction.update, + limit: 3, + offset: 3, + ); + + expect( + source.toJson(), + { + 'action': 2, + 'limit': 3, + 'offset': 3, + }, + ); + }); - test('#toJson', () { - const source = Query( + test('.fromJson', () { + final json = { + 'action': 2, + 'limit': 3, + 'offset': 3, + }; + + final result = Query.fromJson(json); + expect( + result, + const Query( action: QueryAction.update, limit: 3, offset: 3, - ); + ), + ); + }); - expect( - source.toJson(), - { - 'action': 2, - 'limit': 3, - 'offset': 3, - }, - ); + group('.where', () { + test('required arguments', () { + const expandedQuery = Query(where: [Where('id', value: 2)]); + final factoried = Query.where('id', 2); + expect(factoried, expandedQuery); + expect(Where.firstByField('id', factoried.where)!.value, 2); + expect(factoried.unlimited, isTrue); }); - group('factories', () { - test('.fromJson', () { - final json = { - 'action': 2, - 'limit': 3, - 'offset': 3, - }; - - final result = Query.fromJson(json); - expect( - result, - const Query( - action: QueryAction.update, - limit: 3, - offset: 3, - ), - ); - }); - - group('.where', () { - test('required arguments', () { - const expandedQuery = Query(where: [Where('id', value: 2)]); - final factoried = Query.where('id', 2); - expect(factoried, expandedQuery); - expect(Where.firstByField('id', factoried.where)!.value, 2); - expect(factoried.unlimited, isTrue); - }); - - test('limit1:true', () { - const expandedQuery = Query(where: [Where('id', value: 2)], limit: 1); - final factoried = Query.where('id', 2, limit1: true); - expect(factoried, expandedQuery); - expect(factoried.unlimited, isFalse); - }); - }); + test('limit1:true', () { + const expandedQuery = Query(where: [Where('id', value: 2)], limit: 1); + final factoried = Query.where('id', 2, limit1: true); + expect(factoried, expandedQuery); + expect(factoried.unlimited, isFalse); }); }); } diff --git a/packages/brick_rest/analysis_options.yaml b/packages/brick_rest/analysis_options.yaml index f04c6cf0..5008bf6c 100644 --- a/packages/brick_rest/analysis_options.yaml +++ b/packages/brick_rest/analysis_options.yaml @@ -1 +1,326 @@ include: ../../analysis_options.yaml + +linter: + rules: + # This list is derived from the list of all available lints located at + # https://github.com/dart-lang/linter/blob/master/example/all.yaml + - always_declare_return_types + # - always_put_control_body_on_new_line + # - always_put_required_named_parameters_first + # - always_specify_types + - always_use_package_imports + - annotate_overrides + # - avoid_annotating_with_dynamic + - avoid_bool_literals_in_conditional_expressions + # - avoid_catches_without_on_clauses + # - avoid_catching_errors + - avoid_classes_with_only_static_members + - 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_final_parameters + - avoid_function_literals_in_foreach_calls + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_multiple_declarations_per_line + - avoid_null_checks_in_equality_operators + # - avoid_positional_boolean_parameters + - avoid_print + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - 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_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - cast_nullable_to_non_nullable + # - close_sinks + - collection_methods_unrelated_type + - combinators_ordering + - comment_references + - conditional_uri_does_not_exist + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - dangling_library_doc_comments + - depend_on_referenced_packages + - deprecated_consistency + - deprecated_member_use_from_same_package + # - diagnostic_describe_all_properties + - directives_ordering + - discarded_futures + - do_not_use_environment + - empty_catches + - empty_constructor_bodies + - empty_statements + - eol_at_end_of_file + - exhaustive_cases + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - implicit_call_tearoffs + - implicit_reopen + - invalid_case_patterns + - join_return_with_assignment + # - leading_newlines_in_multiline_strings + - library_annotations + - library_names + - library_prefixes + - library_private_types_in_public_api + # - lines_longer_than_80_chars + - literal_only_boolean_expressions + - matching_super_parameters + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_default_cases + - no_duplicate_case_values + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers + - no_literal_bool_comparisons + - no_logic_in_create_state + - no_runtimeType_toString + - no_self_assignments + - no_wildcard_variable_uses + - non_constant_identifier_names + - noop_primitive_operations + - null_check_on_nullable_type_parameter + - null_closures + - omit_local_variable_types + - 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_double_quotes + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + # - prefer_final_parameters + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - 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_method_calls + - prefer_null_aware_operators + # - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - public_member_api_docs + - recursive_getters + - require_trailing_commas + - secure_pubspec_urls + - sized_box_for_whitespace + - sized_box_shrink_expand + - slash_for_doc_comments + - sort_child_properties_last + # - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - tighten_type_of_initializing_formals + - type_annotate_public_apis + - type_init_formals + - type_literal_in_constant_pattern + - unawaited_futures + # - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_breaks + - unnecessary_const + - unnecessary_constructor_name + # - unnecessary_final + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_late + - unnecessary_library_directive + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_aware_operator_on_extension_on_nullable + - 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 + - unnecessary_to_list_in_spreads + # - unreachable_from_main + - unrelated_type_equality_checks + - unsafe_html + - use_build_context_synchronously + - use_colored_box + - use_decorated_box + - use_enums + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_if_null_to_convert_nulls_to_bools + - use_is_even_rather_than_modulo + - use_key_in_widget_constructors + - 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_string_in_part_of_directives + - use_super_parameters + - use_test_throws_matchers + - use_to_and_as_if_applicable + - valid_regexps + - void_checks + +analyzer: + exclude: + - example/ + - "**/example/" + - example_rest + - example_graphql + - "**/*.g.dart" + + errors: + # override custom + always_use_package_imports: error + camel_case_extensions: error + camel_case_types: error + curly_braces_in_flow_control_structures: error + directives_ordering: error + file_names: error + prefer_single_quotes: error + prefer_is_empty: error + prefer_is_not_empty: error + require_trailing_commas: error + sort_pub_dependencies: error + unnecessary_statements: error + + # override flutter_lints + avoid_print: error + avoid_unnecessary_containers: error + avoid_web_libraries_in_flutter: error + no_logic_in_create_state: error + prefer_const_constructors: error + prefer_const_constructors_in_immutables: error + prefer_const_declarations: error + prefer_const_literals_to_create_immutables: error + sized_box_for_whitespace: error + sort_child_properties_last: error + use_build_context_synchronously: error + use_full_hex_values_for_flutter_colors: error + use_key_in_widget_constructors: error + + # override recommended lints + always_require_non_null_named_parameters: error + annotate_overrides: error + avoid_function_literals_in_foreach_calls: error + avoid_init_to_null: error + avoid_null_checks_in_equality_operators: error + avoid_renaming_method_parameters: error + avoid_return_types_on_setters: error + avoid_returning_null_for_void: error + avoid_single_cascade_in_expression_statements: error + await_only_futures: error + constant_identifier_names: error + control_flow_in_finally: error + depend_on_referenced_packages: ignore + empty_constructor_bodies: error + empty_statements: error + exhaustive_cases: error + implementation_imports: ignore + invalid_case_patterns: error + library_names: error + library_prefixes: error + library_private_types_in_public_api: error + matching_super_parameters: error + no_leading_underscores_for_library_prefixes: error + no_leading_underscores_for_local_identifiers: error + no_literal_bool_comparisons: error + null_check_on_nullable_type_parameter: error + null_closures: error + overridden_fields: error + package_names: error + prefer_adjacent_string_concatenation: error + prefer_collection_literals: error + prefer_conditional_assignment: error + prefer_contains: error + prefer_equal_for_default_values: error + prefer_final_fields: error + prefer_for_elements_to_map_fromIterable: error + prefer_function_declarations_over_variables: error + prefer_if_null_operators: error + prefer_initializing_formals: error + prefer_inlined_adds: error + prefer_interpolation_to_compose_strings: error + prefer_is_not_operator: error + prefer_null_aware_operators: error + prefer_spread_collections: error + prefer_void_to_null: error + recursive_getters: error + slash_for_doc_comments: error + type_init_formals: error + type_literal_in_constant_pattern: error + unnecessary_brace_in_string_interps: error + unnecessary_breaks: error + unnecessary_const: error + unnecessary_constructor_name: error + unnecessary_getters_setters: error + unnecessary_late: error + unnecessary_new: error + unnecessary_null_aware_assignments: error + unnecessary_null_in_if_null_operators: error + unnecessary_nullable_for_final_variable_declarations: error + unnecessary_string_escapes: error + unnecessary_string_interpolations: error + unnecessary_this: error + use_function_type_syntax_for_parameters: error + use_rethrow_when_possible: error diff --git a/packages/brick_rest/lib/brick_rest.dart b/packages/brick_rest/lib/brick_rest.dart index 5668c7b6..a8dcbd82 100644 --- a/packages/brick_rest/lib/brick_rest.dart +++ b/packages/brick_rest/lib/brick_rest.dart @@ -6,5 +6,6 @@ export 'package:brick_rest/src/rest_adapter.dart'; export 'package:brick_rest/src/rest_model.dart'; export 'package:brick_rest/src/rest_model_dictionary.dart'; export 'package:brick_rest/src/rest_provider.dart'; +export 'package:brick_rest/src/rest_provider_query.dart'; export 'package:brick_rest/src/rest_request.dart'; export 'package:brick_rest/src/rest_request_transformer.dart'; diff --git a/packages/brick_rest/lib/gzip_http_client.dart b/packages/brick_rest/lib/gzip_http_client.dart index 559ffe27..82a12c66 100644 --- a/packages/brick_rest/lib/gzip_http_client.dart +++ b/packages/brick_rest/lib/gzip_http_client.dart @@ -15,6 +15,8 @@ class GZipHttpClient extends http.BaseClient { @protected final http.Client innerClient; + /// Gzip all incoming requests and mutate them so that the payload is encoded. + /// Additionally, (over)writes the header `{'Content-Encoding': 'gzip'}`. GZipHttpClient({ http.Client? innerClient, diff --git a/packages/brick_rest/lib/rest_exception.dart b/packages/brick_rest/lib/rest_exception.dart index 669bb25a..47468614 100644 --- a/packages/brick_rest/lib/rest_exception.dart +++ b/packages/brick_rest/lib/rest_exception.dart @@ -1,11 +1,14 @@ import 'dart:convert'; +import 'package:brick_rest/src/rest_provider.dart'; import 'package:http/http.dart' as http; /// An error class exclusive to the [RestProvider] class RestException implements Exception { + /// The HTTP response that triggered the exception final http.Response response; + /// An error class exclusive to the [RestProvider] RestException(this.response); /// Decoded error messages if included under the top-level key 'errors' in the response. @@ -22,6 +25,7 @@ class RestException implements Exception { return null; } + /// A string representation of the exception String get message => 'statusCode=${response.statusCode} url=${response.request?.url} method=${response.request?.method} body=${response.body}'; diff --git a/packages/brick_rest/lib/src/annotations/rest.dart b/packages/brick_rest/lib/src/annotations/rest.dart index 7c5a6980..70bb4944 100644 --- a/packages/brick_rest/lib/src/annotations/rest.dart +++ b/packages/brick_rest/lib/src/annotations/rest.dart @@ -1,6 +1,6 @@ import 'package:brick_core/field_serializable.dart'; -/// An annotation used to specify how a field is serialized for a [RestAdapter]. +/// An annotation used to specify how a field is serialized for a `RestAdapter`. /// Heavily inspired by [JsonKey](https://github.com/dart-lang/json_serializable/blob/master/json_annotation/lib/src/json_key.dart) class Rest implements FieldSerializable { /// The value to use if the source does not contain this key or if the diff --git a/packages/brick_rest/lib/src/rest_adapter.dart b/packages/brick_rest/lib/src/rest_adapter.dart index 25c8a358..7b9bc68c 100644 --- a/packages/brick_rest/lib/src/rest_adapter.dart +++ b/packages/brick_rest/lib/src/rest_adapter.dart @@ -4,11 +4,12 @@ import 'package:brick_rest/src/rest_provider.dart'; import 'package:brick_rest/src/rest_request_transformer.dart'; class _DefaultRestTransformer extends RestRequestTransformer { - const _DefaultRestTransformer(Query? query, RestModel? instance) : super(null, null); + const _DefaultRestTransformer(Query? _, RestModel? __) : super(null, null); } /// Constructors that convert app models to and from REST abstract class RestAdapter implements Adapter { + /// Deserialize data from a REST response Future fromRest( Map input, { required RestProvider provider, @@ -18,6 +19,7 @@ abstract class RestAdapter implements Adapter /// The endpoint path to access provided a query. Must include a leading slash. RestRequestTransformer Function(Query?, TModel?)? get restRequest => _DefaultRestTransformer.new; + /// Serialize data to a REST endpoint Future> toRest( TModel input, { required RestProvider provider, diff --git a/packages/brick_rest/lib/src/rest_model.dart b/packages/brick_rest/lib/src/rest_model.dart index 564963c2..57ec901a 100644 --- a/packages/brick_rest/lib/src/rest_model.dart +++ b/packages/brick_rest/lib/src/rest_model.dart @@ -1,4 +1,4 @@ import 'package:brick_core/core.dart'; -/// Models accessible to the [RestProvider] +/// Models accessible to the `RestProvider` abstract class RestModel implements Model {} diff --git a/packages/brick_rest/lib/src/rest_model_dictionary.dart b/packages/brick_rest/lib/src/rest_model_dictionary.dart index 07c18d71..bb6d8d1b 100644 --- a/packages/brick_rest/lib/src/rest_model_dictionary.dart +++ b/packages/brick_rest/lib/src/rest_model_dictionary.dart @@ -4,5 +4,6 @@ import 'package:brick_rest/src/rest_model.dart'; /// Associates app models with their [RestAdapter] class RestModelDictionary extends ModelDictionary> { + /// Associates app models with their [RestAdapter] const RestModelDictionary(super.adapterFor); } diff --git a/packages/brick_rest/lib/src/rest_provider.dart b/packages/brick_rest/lib/src/rest_provider.dart index 0dc799cb..b349949f 100644 --- a/packages/brick_rest/lib/src/rest_provider.dart +++ b/packages/brick_rest/lib/src/rest_provider.dart @@ -1,9 +1,12 @@ +// ignore_for_file: deprecated_member_use + import 'dart:convert'; import 'package:brick_core/core.dart'; import 'package:brick_rest/rest_exception.dart'; import 'package:brick_rest/src/rest_model.dart'; import 'package:brick_rest/src/rest_model_dictionary.dart'; +import 'package:brick_rest/src/rest_provider_query.dart'; import 'package:brick_rest/src/rest_request.dart'; import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; @@ -24,9 +27,11 @@ class RestProvider implements Provider { /// All requests pass through this client. http.Client client; + /// Internal logger @protected final Logger logger; + /// Retrieves data from an HTTP endpoint RestProvider( this.baseEndpoint, { required this.modelDictionary, @@ -36,11 +41,17 @@ class RestProvider implements Provider { /// Sends a DELETE request method to the endpoint @override - Future delete(instance, {query, repository}) async { + Future delete( + TModel instance, { + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final fromAdapter = adapter.restRequest != null ? adapter.restRequest!(query, instance).delete : null; - final request = (query?.providerArgs['request'] as RestRequest?) ?? fromAdapter; + final request = (query?.providerQueries[RestProvider] as RestProviderQuery?)?.request ?? + (query?.providerArgs['request'] as RestRequest?) ?? + fromAdapter; final url = request?.url; if (url == null) return null; @@ -60,10 +71,15 @@ class RestProvider implements Provider { } @override - Future exists({query, repository}) async { + Future exists({ + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final fromAdapter = adapter.restRequest != null ? adapter.restRequest!(query, null).get : null; - final request = (query?.providerArgs['request'] as RestRequest?) ?? fromAdapter; + final request = (query?.providerQueries[RestProvider] as RestProviderQuery?)?.request ?? + (query?.providerArgs['request'] as RestRequest?) ?? + fromAdapter; final url = request?.url; if (url == null) return false; @@ -76,15 +92,16 @@ class RestProvider implements Provider { return statusCodeIsSuccessful(resp.statusCode); } - /// [Query]'s `providerArgs` can extend the [get] functionality: - /// * `'request'` (`RestRequest`) Specifies configurable information about the request like HTTP method or top level key - /// (however, when defined, `['request'].topLevelKey` is prioritized). Note that when no key is defined, the first value is returned - /// regardless of the first key (in the example, `{"id"...}`). @override - Future> get({query, repository}) async { + Future> get({ + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final fromAdapter = adapter.restRequest != null ? adapter.restRequest!(query, null).get : null; - final request = (query?.providerArgs['request'] as RestRequest?) ?? fromAdapter; + final request = (query?.providerQueries[RestProvider] as RestProviderQuery?)?.request ?? + (query?.providerArgs['request'] as RestRequest?) ?? + fromAdapter; final url = request?.url; if (url == null) return []; @@ -101,9 +118,7 @@ class RestProvider implements Provider { final body = parsed is Iterable ? parsed : [parsed]; final results = body .where((msg) => msg != null) - .map((msg) { - return adapter.fromRest(msg, provider: this, repository: repository); - }) + .map((msg) => adapter.fromRest(msg, provider: this, repository: repository)) .toList() .cast>(); @@ -114,15 +129,19 @@ class RestProvider implements Provider { } } - /// [Query]'s `providerArgs` can extend the [upsert] functionality: - /// * `'request'` (`RestRequest`) Specifies configurable information about the request like HTTP method or top level key @override - Future upsert(instance, {query, repository}) async { + Future upsert( + TModel instance, { + Query? query, + ModelRepository? repository, + }) async { final adapter = modelDictionary.adapterFor[TModel]!; final body = await adapter.toRest(instance, provider: this, repository: repository); final fromAdapter = adapter.restRequest != null ? adapter.restRequest!(query, instance).upsert : null; - final request = (query?.providerArgs['request'] as RestRequest?) ?? fromAdapter; + final request = (query?.providerQueries[RestProvider] as RestProviderQuery?)?.request ?? + (query?.providerArgs['request'] as RestRequest?) ?? + fromAdapter; final url = request?.url; if (url == null) return null; @@ -156,7 +175,9 @@ class RestProvider implements Provider { /// Expand a query into HTTP headers @protected Map headersForQuery(Query? query, Map? requestHeaders) { - if ((query == null || query.providerArgs['request']?.headers == null) && + final request = (query?.providerQueries[RestProvider] as RestProviderQuery?)?.request ?? + (query?.providerArgs['request'] as RestRequest?); + if ((query == null || request?.headers == null) && requestHeaders == null && defaultHeaders != null) { return defaultHeaders!; @@ -166,7 +187,7 @@ class RestProvider implements Provider { ..addAll({'Content-Type': 'application/json'}) ..addAll(defaultHeaders ?? {}) ..addAll(requestHeaders ?? {}) - ..addAll(query?.providerArgs['request']?.headers ?? {}); + ..addAll(request?.headers ?? {}); } /// If a [key] is defined from the adapter and it is not null in the response, use it to narrow the response. @@ -194,9 +215,11 @@ class RestProvider implements Provider { Map? body, }) async { final combinedBody = body ?? {}; - final url = Uri.parse([baseEndpoint, request.url!].join('')); - final method = - (query?.providerArgs ?? {})['request']?.method ?? request.method ?? operation.httpMethod; + final url = Uri.parse([baseEndpoint, request.url!].join()); + final requestFromQuery = + (query?.providerQueries[RestProvider] as RestProviderQuery?)?.request ?? + (query?.providerArgs['request'] as RestRequest?); + final method = requestFromQuery?.method ?? request.method ?? operation.httpMethod; final headers = headersForQuery(query, request.headers); logger.fine('$method $url'); @@ -208,8 +231,8 @@ class RestProvider implements Provider { } // if supplementalTopLevelData is specified it, insert alongside normal payload - final topLevelData = (query?.providerArgs ?? {})['request']?.supplementalTopLevelData ?? - request.supplementalTopLevelData; + final topLevelData = + requestFromQuery?.supplementalTopLevelData ?? request.supplementalTopLevelData; if (topLevelData != null) { combinedBody.addAll(topLevelData); } @@ -229,11 +252,12 @@ class RestProvider implements Provider { return await client.put(url, body: serializedBody, headers: headers); default: throw StateError( - "Request method $method is unhandled; use providerArgs['request'] or RestRequest#method", + 'Request method $method is unhandled; use RestProviderQuery or RestRequest#method', ); } } + /// Whether the status code is between 200 and 300 static bool statusCodeIsSuccessful(int? statusCode) => statusCode != null && 200 <= statusCode && statusCode < 300; } diff --git a/packages/brick_rest/lib/src/rest_provider_query.dart b/packages/brick_rest/lib/src/rest_provider_query.dart new file mode 100644 index 00000000..428e5706 --- /dev/null +++ b/packages/brick_rest/lib/src/rest_provider_query.dart @@ -0,0 +1,27 @@ +import 'package:brick_core/query.dart'; +import 'package:brick_rest/src/rest_provider.dart'; +import 'package:brick_rest/src/rest_request.dart'; + +/// A REST-specific query definitiion for use in [Query]. +class RestProviderQuery extends ProviderQuery { + /// The [RestRequest] to use for the [Query]. + final RestRequest? request; + + /// A REST-specific query definitiion for use in [Query]. + const RestProviderQuery({ + this.request, + }); + + @override + Map toJson() => { + if (request != null) 'request': request?.toJson(), + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RestProviderQuery && runtimeType == other.runtimeType && request == other.request; + + @override + int get hashCode => request.hashCode; +} diff --git a/packages/brick_rest/lib/src/rest_request.dart b/packages/brick_rest/lib/src/rest_request.dart index ecc77ada..2f098b64 100644 --- a/packages/brick_rest/lib/src/rest_request.dart +++ b/packages/brick_rest/lib/src/rest_request.dart @@ -1,5 +1,6 @@ -/// A cohesive definition for [RestRequestTransformer]'s instance fields. +/// A cohesive definition for `RestRequestTransformer`'s instance fields. class RestRequest { + /// HTTP headers final Map? headers; /// The [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) @@ -37,6 +38,7 @@ class RestRequest { /// ``` final String? url; + /// A cohesive definition for `RestRequestTransformer`'s instance fields. const RestRequest({ this.headers, this.method, @@ -45,23 +47,40 @@ class RestRequest { this.url, }); - factory RestRequest.fromJson(Map data) { - return RestRequest( - headers: data['headers'], - method: data['method'], - supplementalTopLevelData: data['supplementalTopLevelData'], - topLevelKey: data['topLevelKey'], - url: data['url'], - ); - } + /// Deserialize a request from JSON + factory RestRequest.fromJson(Map data) => RestRequest( + headers: data['headers'], + method: data['method'], + supplementalTopLevelData: data['supplementalTopLevelData'], + topLevelKey: data['topLevelKey'], + url: data['url'], + ); - Map toJson() { - return { - 'headers': headers, - 'method': method, - 'supplementalTopLevelData': supplementalTopLevelData, - 'topLevelKey': topLevelKey, - 'url': url, - }; - } + /// Serialize a request to JSON + Map toJson() => { + 'headers': headers, + 'method': method, + 'supplementalTopLevelData': supplementalTopLevelData, + 'topLevelKey': topLevelKey, + 'url': url, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RestRequest && + runtimeType == other.runtimeType && + headers == other.headers && + method == other.method && + supplementalTopLevelData == other.supplementalTopLevelData && + topLevelKey == other.topLevelKey && + url == other.url; + + @override + int get hashCode => + headers.hashCode ^ + method.hashCode ^ + supplementalTopLevelData.hashCode ^ + topLevelKey.hashCode ^ + url.hashCode; } diff --git a/packages/brick_rest/lib/src/rest_request_transformer.dart b/packages/brick_rest/lib/src/rest_request_transformer.dart index 4e87155d..3e503675 100644 --- a/packages/brick_rest/lib/src/rest_request_transformer.dart +++ b/packages/brick_rest/lib/src/rest_request_transformer.dart @@ -34,5 +34,20 @@ abstract class RestRequestTransformer { /// The operation used for any inserting or updating data operations. RestRequest? get upsert => null; + /// Specify request formatting (such as `method` or `url`) for each Brick operation. + /// + /// This class should be subclassed for each model. For example: + /// + /// ```dart + /// @RestSerializable( + /// requestTransformer: MyModelOperationTransformer.new, + /// ) + /// class MyModel extends RestModel {} + /// class MyModelOperationTransformer extends RestRequestTransformer { + /// final get = RestRequest( + /// url: 'https://myapi.com/mymodel' + /// ); + /// } + /// ``` const RestRequestTransformer(this.query, this.instance); } diff --git a/packages/brick_rest/test/__mocks__.dart b/packages/brick_rest/test/__mocks__.dart index 73a95524..c35b9eaf 100644 --- a/packages/brick_rest/test/__mocks__.dart +++ b/packages/brick_rest/test/__mocks__.dart @@ -1,3 +1,4 @@ +import 'package:brick_core/src/model_repository.dart'; import 'package:brick_rest/brick_rest.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; @@ -9,9 +10,8 @@ class DemoRestModel extends RestModel { } /// Create [DemoRestModel] from json -Future _$DemoRestModelFromRest(Map json) async { - return DemoRestModel(json['name'] as String); -} +Future _$DemoRestModelFromRest(Map json) async => + DemoRestModel(json['name'] as String); /// Create json from [DemoRestModel] Future> _$DemoRestModelToRest(DemoRestModel instance) async { @@ -48,13 +48,21 @@ class DemoRestRequestTransformer extends RestRequestTransformer { const DemoRestRequestTransformer(super.query, RestModel? super.instance); } -/// Construct a [DemoRestModel] for the [RestRepository] +/// Construct a [DemoRestModel] for the `RestRepository` class DemoRestModelAdapter extends RestAdapter { @override - Future fromRest(data, {required provider, repository}) => + Future fromRest( + Map data, { + required RestProvider provider, + ModelRepository? repository, + }) => _$DemoRestModelFromRest(data); @override - Future> toRest(instance, {required provider, repository}) async => + Future> toRest( + DemoRestModel instance, { + required RestProvider provider, + ModelRepository? repository, + }) async => await _$DemoRestModelToRest(instance); @override @@ -66,13 +74,12 @@ final Map> _restMappings = { }; final restModelDictionary = RestModelDictionary(_restMappings); -MockClient generateClient(String response, {String? requestBody, String? requestMethod}) { - return MockClient((req) async { - final matchesRequestBody = req.body == requestBody || requestBody == null; - final matchesRequestMethod = req.method == requestMethod || requestMethod == null; +MockClient generateClient(String response, {String? requestBody, String? requestMethod}) => + MockClient((req) async { + final matchesRequestBody = req.body == requestBody || requestBody == null; + final matchesRequestMethod = req.method == requestMethod || requestMethod == null; - if (matchesRequestMethod && matchesRequestBody) return http.Response(response, 200); + if (matchesRequestMethod && matchesRequestBody) return http.Response(response, 200); - throw StateError('No response for $response'); - }); -} + throw StateError('No response for $response'); + }); diff --git a/packages/brick_rest/test/rest_provider_query_test.dart b/packages/brick_rest/test/rest_provider_query_test.dart new file mode 100644 index 00000000..4fe78a03 --- /dev/null +++ b/packages/brick_rest/test/rest_provider_query_test.dart @@ -0,0 +1,141 @@ +import 'package:brick_core/core.dart'; +import 'package:brick_rest/brick_rest.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:test/test.dart'; + +import '__mocks__.dart'; + +RestProvider generateProvider(String response, {String? requestBody, String? requestMethod}) => + RestProvider( + 'http://0.0.0.0:3000', + modelDictionary: restModelDictionary, + client: generateClient(response, requestBody: requestBody, requestMethod: requestMethod), + ); + +void main() { + group('RestProviderQuery', () { + test('#headers', () async { + final provider = RestProvider( + 'http://0.0.0.0:3000', + modelDictionary: restModelDictionary, + client: MockClient((req) async { + if (req.method == 'POST' && req.headers['Authorization'] == 'Basic xyz') { + return http.Response('{"name": "Thomas"}', 200); + } + + throw StateError('No response'); + }), + ); + + final instance = DemoRestModel('Guy'); + const query = Query( + forProviders: [ + RestProviderQuery( + request: RestRequest(headers: {'Authorization': 'Basic xyz'}, url: '/'), + ), + ], + ); + final resp = await provider.upsert(instance, query: query); + + expect(resp!.statusCode, 200); + expect(resp.body, '{"name": "Thomas"}'); + }); + + test('#method PUT', () async { + final provider = generateProvider('{"name": "Guy"}', requestMethod: 'PUT'); + + final instance = DemoRestModel('Guy'); + const query = + Query(forProviders: [RestProviderQuery(request: RestRequest(method: 'PUT', url: '/'))]); + final resp = await provider.upsert(instance, query: query); + + expect(resp!.statusCode, 200); + expect(resp.body, '{"name": "Guy"}'); + }); + + test('#method PATCH', () async { + final provider = generateProvider('{"name": "Guy"}', requestMethod: 'PATCH'); + + final instance = DemoRestModel('Guy'); + const query = Query( + forProviders: [RestProviderQuery(request: RestRequest(method: 'PATCH', url: '/'))], + ); + final resp = await provider.upsert(instance, query: query); + + expect(resp!.statusCode, 200); + expect(resp.body, '{"name": "Guy"}'); + }); + + test('#topLevelKey', () async { + final provider = generateProvider( + '{"name": "Thomas"}', + requestMethod: 'POST', + requestBody: '{"top":{"name":"Guy"}}', + ); + + final instance = DemoRestModel('Guy'); + const query = Query( + forProviders: [RestProviderQuery(request: RestRequest(topLevelKey: 'top', url: '/'))], + ); + final resp = await provider.upsert(instance, query: query); + + expect(resp!.statusCode, 200); + expect(resp.body, '{"name": "Thomas"}'); + }); + + group('#supplementalTopLevelData', () { + test('#get', () async { + final provider = generateProvider( + '[{"name": "Thomas"}]', + requestMethod: 'POST', + requestBody: '{"other_name":{"first_name":"Thomas"}}', + ); + + const query = Query( + forProviders: [ + RestProviderQuery( + request: RestRequest( + url: '/', + method: 'POST', + supplementalTopLevelData: { + 'other_name': {'first_name': 'Thomas'}, + }, + ), + ), + ], + ); + final instance = await provider.get(query: query); + + expect(instance.first.name, 'Thomas'); + }); + + test('#upsert', () async { + final provider = generateProvider( + '{"name": "Thomas"}', + requestMethod: 'POST', + requestBody: '{"top":{"name":"Guy"},"other_name":{"first_name":"Thomas"}}', + ); + + final instance = DemoRestModel('Guy'); + const query = Query( + forProviders: [ + RestProviderQuery( + request: RestRequest( + topLevelKey: 'top', + url: '/', + supplementalTopLevelData: { + 'other_name': {'first_name': 'Thomas'}, + }, + ), + ), + ], + ); + final resp = await provider.upsert(instance, query: query); + + expect(resp!.statusCode, 200); + expect(resp.body, '{"name": "Thomas"}'); + }); + }); + }); +} diff --git a/packages/brick_rest/test/rest_provider_test.dart b/packages/brick_rest/test/rest_provider_test.dart index 8bdf2c2b..e670cdb7 100644 --- a/packages/brick_rest/test/rest_provider_test.dart +++ b/packages/brick_rest/test/rest_provider_test.dart @@ -6,13 +6,12 @@ import 'package:test/test.dart'; import '__mocks__.dart'; -RestProvider generateProvider(String response, {String? requestBody, String? requestMethod}) { - return RestProvider( - 'http://0.0.0.0:3000', - modelDictionary: restModelDictionary, - client: generateClient(response, requestBody: requestBody, requestMethod: requestMethod), - ); -} +RestProvider generateProvider(String response, {String? requestBody, String? requestMethod}) => + RestProvider( + 'http://0.0.0.0:3000', + modelDictionary: restModelDictionary, + client: generateClient(response, requestBody: requestBody, requestMethod: requestMethod), + ); void main() { group('RestProvider', () { @@ -33,9 +32,7 @@ void main() { test('#defaultHeaders', () async { final headers = {'Authorization': 'token=12345'}; - final provider = generateProvider('[{"name": "Guy"}]'); - - provider.defaultHeaders = headers; + final provider = generateProvider('[{"name": "Guy"}]')..defaultHeaders = headers; final instance = await provider.get(); expect(instance.first.name, 'Guy'); }); @@ -64,7 +61,8 @@ void main() { ); final instance = DemoRestModel('Guy'); - final query = Query( + const query = Query( + // ignore: deprecated_member_use providerArgs: { 'request': RestRequest(headers: {'Authorization': 'Basic xyz'}, url: '/'), }, @@ -75,11 +73,39 @@ void main() { expect(resp.body, '{"name": "Thomas"}'); }); + test('RestProviderQuery#headers', () async { + final provider = RestProvider( + 'http://0.0.0.0:3000', + modelDictionary: restModelDictionary, + client: MockClient((req) async { + if (req.method == 'POST' && req.headers['Authorization'] == 'Basic xyz') { + return http.Response('{"name": "Thomas"}', 200); + } + + throw StateError('No response'); + }), + ); + + final instance = DemoRestModel('Guy'); + const query = Query( + forProviders: [ + RestProviderQuery( + request: RestRequest(headers: {'Authorization': 'Basic xyz'}, url: '/'), + ), + ], + ); + final resp = await provider.upsert(instance, query: query); + + expect(resp!.statusCode, 200); + expect(resp.body, '{"name": "Thomas"}'); + }); + test('providerArgs["request"].method PUT', () async { final provider = generateProvider('{"name": "Guy"}', requestMethod: 'PUT'); final instance = DemoRestModel('Guy'); - final query = Query(providerArgs: {'request': RestRequest(method: 'PUT', url: '/')}); + // ignore: deprecated_member_use + const query = Query(providerArgs: {'request': RestRequest(method: 'PUT', url: '/')}); final resp = await provider.upsert(instance, query: query); expect(resp!.statusCode, 200); @@ -90,7 +116,8 @@ void main() { final provider = generateProvider('{"name": "Guy"}', requestMethod: 'PATCH'); final instance = DemoRestModel('Guy'); - final query = Query(providerArgs: {'request': RestRequest(method: 'PATCH', url: '/')}); + // ignore: deprecated_member_use + const query = Query(providerArgs: {'request': RestRequest(method: 'PATCH', url: '/')}); final resp = await provider.upsert(instance, query: query); expect(resp!.statusCode, 200); @@ -105,7 +132,8 @@ void main() { ); final instance = DemoRestModel('Guy'); - final query = Query(providerArgs: {'request': RestRequest(topLevelKey: 'top', url: '/')}); + // ignore: deprecated_member_use + const query = Query(providerArgs: {'request': RestRequest(topLevelKey: 'top', url: '/')}); final resp = await provider.upsert(instance, query: query); expect(resp!.statusCode, 200); @@ -120,7 +148,8 @@ void main() { requestBody: '{"other_name":{"first_name":"Thomas"}}', ); - final query = Query( + const query = Query( + // ignore: deprecated_member_use providerArgs: { 'request': RestRequest( url: '/', @@ -144,7 +173,8 @@ void main() { ); final instance = DemoRestModel('Guy'); - final query = Query( + const query = Query( + // ignore: deprecated_member_use providerArgs: { 'request': RestRequest( topLevelKey: 'top', diff --git a/packages/brick_rest_generators/analysis_options.yaml b/packages/brick_rest_generators/analysis_options.yaml index f04c6cf0..5008bf6c 100644 --- a/packages/brick_rest_generators/analysis_options.yaml +++ b/packages/brick_rest_generators/analysis_options.yaml @@ -1 +1,326 @@ include: ../../analysis_options.yaml + +linter: + rules: + # This list is derived from the list of all available lints located at + # https://github.com/dart-lang/linter/blob/master/example/all.yaml + - always_declare_return_types + # - always_put_control_body_on_new_line + # - always_put_required_named_parameters_first + # - always_specify_types + - always_use_package_imports + - annotate_overrides + # - avoid_annotating_with_dynamic + - avoid_bool_literals_in_conditional_expressions + # - avoid_catches_without_on_clauses + # - avoid_catching_errors + - avoid_classes_with_only_static_members + - 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_final_parameters + - avoid_function_literals_in_foreach_calls + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_multiple_declarations_per_line + - avoid_null_checks_in_equality_operators + # - avoid_positional_boolean_parameters + - avoid_print + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - 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_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - cast_nullable_to_non_nullable + # - close_sinks + - collection_methods_unrelated_type + - combinators_ordering + - comment_references + - conditional_uri_does_not_exist + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - dangling_library_doc_comments + - depend_on_referenced_packages + - deprecated_consistency + - deprecated_member_use_from_same_package + # - diagnostic_describe_all_properties + - directives_ordering + - discarded_futures + - do_not_use_environment + - empty_catches + - empty_constructor_bodies + - empty_statements + - eol_at_end_of_file + - exhaustive_cases + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - implicit_call_tearoffs + - implicit_reopen + - invalid_case_patterns + - join_return_with_assignment + # - leading_newlines_in_multiline_strings + - library_annotations + - library_names + - library_prefixes + - library_private_types_in_public_api + # - lines_longer_than_80_chars + - literal_only_boolean_expressions + - matching_super_parameters + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_default_cases + - no_duplicate_case_values + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers + - no_literal_bool_comparisons + - no_logic_in_create_state + - no_runtimeType_toString + - no_self_assignments + - no_wildcard_variable_uses + - non_constant_identifier_names + - noop_primitive_operations + - null_check_on_nullable_type_parameter + - null_closures + - omit_local_variable_types + - 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_double_quotes + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + # - prefer_final_parameters + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - 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_method_calls + - prefer_null_aware_operators + # - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - public_member_api_docs + - recursive_getters + - require_trailing_commas + - secure_pubspec_urls + - sized_box_for_whitespace + - sized_box_shrink_expand + - slash_for_doc_comments + - sort_child_properties_last + # - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - tighten_type_of_initializing_formals + - type_annotate_public_apis + - type_init_formals + - type_literal_in_constant_pattern + - unawaited_futures + # - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_breaks + - unnecessary_const + - unnecessary_constructor_name + # - unnecessary_final + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_late + - unnecessary_library_directive + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_aware_operator_on_extension_on_nullable + - 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 + - unnecessary_to_list_in_spreads + # - unreachable_from_main + - unrelated_type_equality_checks + - unsafe_html + - use_build_context_synchronously + - use_colored_box + - use_decorated_box + - use_enums + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_if_null_to_convert_nulls_to_bools + - use_is_even_rather_than_modulo + - use_key_in_widget_constructors + - 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_string_in_part_of_directives + - use_super_parameters + - use_test_throws_matchers + - use_to_and_as_if_applicable + - valid_regexps + - void_checks + +analyzer: + exclude: + - example/ + - "**/example/" + - example_rest + - example_graphql + - "**/*.g.dart" + + errors: + # override custom + always_use_package_imports: error + camel_case_extensions: error + camel_case_types: error + curly_braces_in_flow_control_structures: error + directives_ordering: error + file_names: error + prefer_single_quotes: error + prefer_is_empty: error + prefer_is_not_empty: error + require_trailing_commas: error + sort_pub_dependencies: error + unnecessary_statements: error + + # override flutter_lints + avoid_print: error + avoid_unnecessary_containers: error + avoid_web_libraries_in_flutter: error + no_logic_in_create_state: error + prefer_const_constructors: error + prefer_const_constructors_in_immutables: error + prefer_const_declarations: error + prefer_const_literals_to_create_immutables: error + sized_box_for_whitespace: error + sort_child_properties_last: error + use_build_context_synchronously: error + use_full_hex_values_for_flutter_colors: error + use_key_in_widget_constructors: error + + # override recommended lints + always_require_non_null_named_parameters: error + annotate_overrides: error + avoid_function_literals_in_foreach_calls: error + avoid_init_to_null: error + avoid_null_checks_in_equality_operators: error + avoid_renaming_method_parameters: error + avoid_return_types_on_setters: error + avoid_returning_null_for_void: error + avoid_single_cascade_in_expression_statements: error + await_only_futures: error + constant_identifier_names: error + control_flow_in_finally: error + depend_on_referenced_packages: ignore + empty_constructor_bodies: error + empty_statements: error + exhaustive_cases: error + implementation_imports: ignore + invalid_case_patterns: error + library_names: error + library_prefixes: error + library_private_types_in_public_api: error + matching_super_parameters: error + no_leading_underscores_for_library_prefixes: error + no_leading_underscores_for_local_identifiers: error + no_literal_bool_comparisons: error + null_check_on_nullable_type_parameter: error + null_closures: error + overridden_fields: error + package_names: error + prefer_adjacent_string_concatenation: error + prefer_collection_literals: error + prefer_conditional_assignment: error + prefer_contains: error + prefer_equal_for_default_values: error + prefer_final_fields: error + prefer_for_elements_to_map_fromIterable: error + prefer_function_declarations_over_variables: error + prefer_if_null_operators: error + prefer_initializing_formals: error + prefer_inlined_adds: error + prefer_interpolation_to_compose_strings: error + prefer_is_not_operator: error + prefer_null_aware_operators: error + prefer_spread_collections: error + prefer_void_to_null: error + recursive_getters: error + slash_for_doc_comments: error + type_init_formals: error + type_literal_in_constant_pattern: error + unnecessary_brace_in_string_interps: error + unnecessary_breaks: error + unnecessary_const: error + unnecessary_constructor_name: error + unnecessary_getters_setters: error + unnecessary_late: error + unnecessary_new: error + unnecessary_null_aware_assignments: error + unnecessary_null_in_if_null_operators: error + unnecessary_nullable_for_final_variable_declarations: error + unnecessary_string_escapes: error + unnecessary_string_interpolations: error + unnecessary_this: error + use_function_type_syntax_for_parameters: error + use_rethrow_when_possible: error diff --git a/packages/brick_rest_generators/lib/rest_model_serdes_generator.dart b/packages/brick_rest_generators/lib/rest_model_serdes_generator.dart index 421a4804..2c916275 100644 --- a/packages/brick_rest_generators/lib/rest_model_serdes_generator.dart +++ b/packages/brick_rest_generators/lib/rest_model_serdes_generator.dart @@ -14,6 +14,8 @@ class RestModelSerdesGenerator extends ProviderSerializableGenerator { + /// Generate a function to produce a [ClassElement] from REST data RestDeserialize( super.element, super.fields, { diff --git a/packages/brick_rest_generators/lib/src/rest_fields.dart b/packages/brick_rest_generators/lib/src/rest_fields.dart index 790d1cc5..5a2abc89 100644 --- a/packages/brick_rest_generators/lib/src/rest_fields.dart +++ b/packages/brick_rest_generators/lib/src/rest_fields.dart @@ -8,12 +8,14 @@ import 'package:brick_rest_generators/src/rest_serializable_extended.dart'; /// Find `@Rest` given a field class RestAnnotationFinder extends AnnotationFinder with AnnotationFinderWithFieldRename { + /// final RestSerializable? config; + /// Find `@Rest` given a field RestAnnotationFinder([this.config]); @override - Rest from(element) { + Rest from(FieldElement element) { final obj = objectForField(element); if (obj == null) { @@ -51,8 +53,11 @@ class RestAnnotationFinder extends AnnotationFinder class RestFields extends FieldsForClass { @override final RestAnnotationFinder finder; + + /// final RestSerializableExtended? config; + /// Converts all fields to [Rest]s for later consumption RestFields(ClassElement element, [this.config]) : finder = RestAnnotationFinder(config), super(element: element); diff --git a/packages/brick_rest_generators/lib/src/rest_serdes_generator.dart b/packages/brick_rest_generators/lib/src/rest_serdes_generator.dart index c027060f..593ba340 100644 --- a/packages/brick_rest_generators/lib/src/rest_serdes_generator.dart +++ b/packages/brick_rest_generators/lib/src/rest_serdes_generator.dart @@ -2,7 +2,9 @@ import 'package:brick_json_generators/json_serdes_generator.dart'; import 'package:brick_rest/brick_rest.dart'; import 'package:brick_rest_generators/src/rest_fields.dart'; +/// abstract class RestSerdesGenerator extends JsonSerdesGenerator { + /// RestSerdesGenerator( super.element, RestFields super.fields, { diff --git a/packages/brick_rest_generators/lib/src/rest_serializable_extended.dart b/packages/brick_rest_generators/lib/src/rest_serializable_extended.dart index f9bc685e..6fd89964 100644 --- a/packages/brick_rest_generators/lib/src/rest_serializable_extended.dart +++ b/packages/brick_rest_generators/lib/src/rest_serializable_extended.dart @@ -4,8 +4,10 @@ import 'package:brick_rest/brick_rest.dart'; /// however, the function can't be re-interpreted by ConstantReader. /// So the name is grabbed to be used in a later generator. class RestSerializableExtended extends RestSerializable { + /// final String? requestName; + /// const RestSerializableExtended({ super.fieldRename, super.nullable, diff --git a/packages/brick_rest_generators/lib/src/rest_serialize.dart b/packages/brick_rest_generators/lib/src/rest_serialize.dart index 9392c54b..884cde31 100644 --- a/packages/brick_rest_generators/lib/src/rest_serialize.dart +++ b/packages/brick_rest_generators/lib/src/rest_serialize.dart @@ -5,6 +5,7 @@ import 'package:brick_rest_generators/src/rest_serdes_generator.dart'; /// Generate a function to produce a [ClassElement] to REST data class RestSerialize extends RestSerdesGenerator with JsonSerialize { + /// RestSerialize( super.element, super.fields, { diff --git a/packages/brick_rest_generators/pubspec.yaml b/packages/brick_rest_generators/pubspec.yaml index 43de0df8..1379cf74 100644 --- a/packages/brick_rest_generators/pubspec.yaml +++ b/packages/brick_rest_generators/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: source_gen: ">=1.2.2 <2.0.0" dev_dependencies: - test: ^1.20.1 - lints: ^2.0.1 brick_build_test: path: ../brick_build_test + lints: + test: diff --git a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_constructor_member_field_mismatch.dart b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_constructor_member_field_mismatch.dart index 7cff47f7..9781efd9 100644 --- a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_constructor_member_field_mismatch.dart +++ b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_constructor_member_field_mismatch.dart @@ -1,6 +1,6 @@ import 'package:brick_rest/brick_rest.dart'; -final output = r''' +const output = r''' Future _$RestConstructorMemberFieldMismatchFromRest(Map data, {required RestProvider provider, diff --git a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_enum_as_string.dart b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_enum_as_string.dart index c6f3b554..5ac05580 100644 --- a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_enum_as_string.dart +++ b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_enum_as_string.dart @@ -1,6 +1,6 @@ import 'package:brick_rest/brick_rest.dart'; -final output = r''' +const output = r''' Future _$EnumAsStringFromRest(Map data, {required RestProvider provider, RestFirstRepository? repository}) async { return EnumAsString( diff --git a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_ignore_from_to.dart b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_ignore_from_to.dart index 41d26d5d..85ac951d 100644 --- a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_ignore_from_to.dart +++ b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_ignore_from_to.dart @@ -1,6 +1,6 @@ import 'package:brick_rest/brick_rest.dart'; -final output = r''' +const output = r''' Future _$RestIgnoreFromToFromRest(Map data, {required RestProvider provider, RestFirstRepository? repository}) async { return RestIgnoreFromTo( diff --git a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_unserializable_field_with_generator.dart b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_unserializable_field_with_generator.dart index a67923cf..0482c507 100644 --- a/packages/brick_rest_generators/test/rest_model_serdes_generator/test_unserializable_field_with_generator.dart +++ b/packages/brick_rest_generators/test/rest_model_serdes_generator/test_unserializable_field_with_generator.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:brick_rest/brick_rest.dart'; -final output = r''' +const output = r''' Future _$RestUnserializableFieldWithGeneratorFromRest(Map data, {required RestProvider provider, diff --git a/packages/brick_rest_generators/test/rest_model_serdes_generator_test.dart b/packages/brick_rest_generators/test/rest_model_serdes_generator_test.dart index e46d241b..e5def7c5 100644 --- a/packages/brick_rest_generators/test/rest_model_serdes_generator_test.dart +++ b/packages/brick_rest_generators/test/rest_model_serdes_generator_test.dart @@ -14,7 +14,7 @@ import 'rest_model_serdes_generator/test_unserializable_field_with_generator.dar as unserializable_field_with_generator; final _generator = TestGenerator(); -final folder = 'rest_model_serdes_generator'; +const folder = 'rest_model_serdes_generator'; final generateReader = generateLibraryForFolder(folder); void main() {