From e348c921d5e621975e4f06d3eeff8e8a89dda109 Mon Sep 17 00:00:00 2001 From: Niels Gorter Date: Fri, 7 Oct 2022 12:16:17 +0200 Subject: [PATCH] initial commit --- .gitignore | 30 +++++ .metadata | 10 ++ CHANGELOG.md | 3 + LICENSE | 21 ++++ README.md | 15 +++ analysis_options.yaml | 4 + lib/flutter_data_interface.dart | 99 ++++++++++++++++ pubspec.yaml | 21 ++++ test/flutter_data_interface_test.dart | 157 ++++++++++++++++++++++++++ 9 files changed, 360 insertions(+) create mode 100644 .gitignore create mode 100644 .metadata create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 analysis_options.yaml create mode 100644 lib/flutter_data_interface.dart create mode 100644 pubspec.yaml create mode 100644 test/flutter_data_interface_test.dart diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96486fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..7b724eb --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# 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. + +version: + revision: ffccd96b62ee8cec7740dab303538c5fc26ac543 + channel: stable + +project_type: package diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..275d71f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9e44913 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Iconica + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..755b7d2 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +[![style: effective dart](https://img.shields.io/badge/style-effective_dart-40c4ff.svg)](https://github.com/tenhobi/effective_dart) + +Flutter package for creating data providers + +## Issues + +Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_axis_video_viewer/issues) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl). + +## Want to contribute + +If you would like to contribute to the plugin (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_date_interface/pulls). + +## Author + +This [flutter_data_interface](https://github.com/Iconica-Development/flutter_data_interface) for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/lib/flutter_data_interface.dart b/lib/flutter_data_interface.dart new file mode 100644 index 0000000..8811e4e --- /dev/null +++ b/lib/flutter_data_interface.dart @@ -0,0 +1,99 @@ +library plugin_platform_interface; + +import 'package:meta/meta.dart'; + +/// Base class for data interfaces. +/// +/// Provides a static helper method for ensuring that data interfaces are +/// implemented using `extends` instead of `implements`. +/// +/// Data interface classes are expected to have a private static token object which will be +/// be passed to [verify] along with a data interface object for verification. +/// +/// Sample usage: +/// +/// +/// Mockito mocks of data interfaces will fail the verification, in test code only it is possible +/// to include the [MockDataInterfaceMixin] for the verification to be temporarily disabled. See +/// [MockDataInterfaceMixin] for a sample of using Mockito to mock a data interface. +abstract class DataInterface { + /// Constructs a DataInterface, for use only in constructors of abstract + /// derived classes. + /// + /// @param token The same, non-`const` `Object` that will be passed to `verify`. + DataInterface({required Object token}) { + _instanceTokens[this] = token; + } + + /// Expando mapping instances of DataInterface to their associated tokens. + /// The reason this is not simply a private field of type `Object?` is because + /// as of the implementation of field promotion in Dart + /// (https://github.com/dart-lang/language/issues/2020), it is a runtime error + /// to invoke a private member that is mocked in another library. The expando + /// approach prevents [_verify] from triggering this runtime exception when + /// encountering an implementation that uses `implements` rather than + /// `extends`. This in turn allows [_verify] to throw an [AssertionError] (as + /// documented). + static final Expando _instanceTokens = Expando(); + + /// Ensures that the data instance was constructed with a non-`const` token + /// that matches the provided token and throws [AssertionError] if not. + /// + /// This is used to ensure that implementers are using `extends` rather than + /// `implements`. + /// + /// Subclasses of [MockDataInterfaceMixin] are assumed to be valid in debug + /// builds. + /// + /// This is implemented as a static method so that it cannot be overridden + /// with `noSuchMethod`. + static void verify(DataInterface instance, Object token) { + _verify(instance, token, preventConstObject: true); + } + + /// Performs the same checks as `verify` but without throwing an + /// [AssertionError] if `const Object()` is used as the instance token. + /// + /// This method will be deprecated in a future release. + static void verifyToken(DataInterface instance, Object token) { + _verify(instance, token, preventConstObject: false); + } + + static void _verify( + DataInterface instance, + Object token, { + required bool preventConstObject, + }) { + if (instance is MockDataInterfaceMixin) { + bool assertionsEnabled = false; + assert(() { + assertionsEnabled = true; + return true; + }()); + if (!assertionsEnabled) { + throw AssertionError( + '`MockDataInterfaceMixin` is not intended for use in release builds.'); + } + return; + } + if (preventConstObject && + identical(_instanceTokens[instance], const Object())) { + throw AssertionError('`const Object()` cannot be used as the token.'); + } + if (!identical(token, _instanceTokens[instance])) { + throw AssertionError( + 'Data interfaces must not be implemented with `implements`'); + } + } +} + +/// A [DataInterface] mixin that can be combined with fake or mock objects, +/// such as test's `Fake` or mockito's `Mock`. +/// +/// It passes the [DataInterface.verify] check even though it isn't +/// using `extends`. +/// +/// This class is intended for use in tests only. +/// +@visibleForTesting +abstract class MockDataInterfaceMixin implements DataInterface {} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..58a58d2 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,21 @@ +name: flutter_data_interface +description: Generic data interface package +version: 1.0.0 +repository: https://github.com/Iconica-Development/flutter_data_interface + + +environment: + sdk: '>=2.18.0 <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + mockito: any + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: diff --git a/test/flutter_data_interface_test.dart b/test/flutter_data_interface_test.dart new file mode 100644 index 0000000..7e99c7a --- /dev/null +++ b/test/flutter_data_interface_test.dart @@ -0,0 +1,157 @@ +import 'package:flutter_data_interface/flutter_data_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +class SamplePluginPlatform extends DataInterface { + SamplePluginPlatform() : super(token: _token); + + static final Object _token = Object(); + + // ignore: avoid_setters_without_getters + static set instance(SamplePluginPlatform instance) { + DataInterface.verify(instance, _token); + // A real implementation would set a static instance field here. + } +} + +class ImplementsSamplePluginPlatform extends Mock + implements SamplePluginPlatform {} + +class ImplementsSamplePluginPlatformUsingNoSuchMethod + implements SamplePluginPlatform { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class ImplementsSamplePluginPlatformUsingMockPlatformInterfaceMixin extends Mock + with MockDataInterfaceMixin + implements SamplePluginPlatform {} + +class ImplementsSamplePluginPlatformUsingFakePlatformInterfaceMixin extends Fake + with MockDataInterfaceMixin + implements SamplePluginPlatform {} + +class ExtendsSamplePluginPlatform extends SamplePluginPlatform {} + +class ConstTokenPluginPlatform extends DataInterface { + ConstTokenPluginPlatform() : super(token: _token); + + static const Object _token = Object(); // invalid + + // ignore: avoid_setters_without_getters + static set instance(ConstTokenPluginPlatform instance) { + DataInterface.verify(instance, _token); + } +} + +class ExtendsConstTokenPluginPlatform extends ConstTokenPluginPlatform {} + +class VerifyTokenPluginPlatform extends DataInterface { + VerifyTokenPluginPlatform() : super(token: _token); + + static final Object _token = Object(); + + // ignore: avoid_setters_without_getters + static set instance(VerifyTokenPluginPlatform instance) { + DataInterface.verifyToken(instance, _token); + // A real implementation would set a static instance field here. + } +} + +class ImplementsVerifyTokenPluginPlatform extends Mock + implements VerifyTokenPluginPlatform {} + +class ImplementsVerifyTokenPluginPlatformUsingMockPlatformInterfaceMixin + extends Mock + with MockDataInterfaceMixin + implements VerifyTokenPluginPlatform {} + +class ExtendsVerifyTokenPluginPlatform extends VerifyTokenPluginPlatform {} + +class ConstVerifyTokenPluginPlatform extends DataInterface { + ConstVerifyTokenPluginPlatform() : super(token: _token); + + static const Object _token = Object(); // invalid + + // ignore: avoid_setters_without_getters + static set instance(ConstVerifyTokenPluginPlatform instance) { + DataInterface.verifyToken(instance, _token); + } +} + +class ImplementsConstVerifyTokenPluginPlatform extends DataInterface + implements ConstVerifyTokenPluginPlatform { + ImplementsConstVerifyTokenPluginPlatform() : super(token: const Object()); +} + +// Ensures that `PlatformInterface` has no instance methods. Adding an +// instance method is discouraged and may be a breaking change if it +// conflicts with instance methods in subclasses. +class StaticMethodsOnlyPlatformInterfaceTest implements DataInterface {} + +class StaticMethodsOnlyMockPlatformInterfaceMixinTest + implements MockDataInterfaceMixin {} + +void main() { + group('`verify`', () { + test('prevents implementation with `implements`', () { + expect(() { + SamplePluginPlatform.instance = ImplementsSamplePluginPlatform(); + }, throwsA(isA())); + }); + + test('prevents implmentation with `implements` and `noSuchMethod`', () { + expect(() { + SamplePluginPlatform.instance = + ImplementsSamplePluginPlatformUsingNoSuchMethod(); + }, throwsA(isA())); + }); + + test('allows mocking with `implements`', () { + final SamplePluginPlatform mock = + ImplementsSamplePluginPlatformUsingMockPlatformInterfaceMixin(); + SamplePluginPlatform.instance = mock; + }); + + test('allows faking with `implements`', () { + final SamplePluginPlatform fake = + ImplementsSamplePluginPlatformUsingFakePlatformInterfaceMixin(); + SamplePluginPlatform.instance = fake; + }); + + test('allows extending', () { + SamplePluginPlatform.instance = ExtendsSamplePluginPlatform(); + }); + + test('prevents `const Object()` token', () { + expect(() { + ConstTokenPluginPlatform.instance = ExtendsConstTokenPluginPlatform(); + }, throwsA(isA())); + }); + }); + + // Tests of the earlier, to-be-deprecated `verifyToken` method + group('`verifyToken`', () { + test('prevents implementation with `implements`', () { + expect(() { + VerifyTokenPluginPlatform.instance = + ImplementsVerifyTokenPluginPlatform(); + }, throwsA(isA())); + }); + + test('allows mocking with `implements`', () { + final VerifyTokenPluginPlatform mock = + ImplementsVerifyTokenPluginPlatformUsingMockPlatformInterfaceMixin(); + VerifyTokenPluginPlatform.instance = mock; + }); + + test('allows extending', () { + VerifyTokenPluginPlatform.instance = ExtendsVerifyTokenPluginPlatform(); + }); + + test('does not prevent `const Object()` token', () { + ConstVerifyTokenPluginPlatform.instance = + ImplementsConstVerifyTokenPluginPlatform(); + }); + }); +}