From dd55b2e138c752442e5da7827e10faee273cb2ec Mon Sep 17 00:00:00 2001 From: beeth0ven Date: Thu, 30 Jun 2022 11:14:40 +0800 Subject: [PATCH] initial `Scope` --- lib/scopes.dart | 1 + .../scopes/configurables/configurable.dart | 8 + .../scopes/scope_methods/scope_expose.dart | 9 + lib/src/scopes/scope_methods/scope_get.dart | 9 + lib/src/scopes/scopes.dart | 6 + lib/src/scopes/scopes/configurable_scope.dart | 44 +++++ lib/src/scopes/scopes/scope.dart | 14 ++ lib/src/scopes/shared/build_scope.dart | 31 ++++ test/scopes/scopes/scope_test.dart | 155 ++++++++++++++++++ test/scopes/scopes_test.dart | 6 + test/scopes_test.dart | 2 + test/toolbox/mock_configurable.dart | 13 ++ 12 files changed, 298 insertions(+) create mode 100644 lib/src/scopes/configurables/configurable.dart create mode 100644 lib/src/scopes/scope_methods/scope_expose.dart create mode 100644 lib/src/scopes/scope_methods/scope_get.dart create mode 100644 lib/src/scopes/scopes.dart create mode 100644 lib/src/scopes/scopes/configurable_scope.dart create mode 100644 lib/src/scopes/scopes/scope.dart create mode 100644 lib/src/scopes/shared/build_scope.dart create mode 100644 test/scopes/scopes/scope_test.dart create mode 100644 test/scopes/scopes_test.dart create mode 100644 test/toolbox/mock_configurable.dart diff --git a/lib/scopes.dart b/lib/scopes.dart index 6773c7b..eb8c92d 100644 --- a/lib/scopes.dart +++ b/lib/scopes.dart @@ -1,2 +1,3 @@ export 'src/observables/observables.dart'; +export 'src/scopes/scopes.dart'; diff --git a/lib/src/scopes/configurables/configurable.dart b/lib/src/scopes/configurables/configurable.dart new file mode 100644 index 0000000..0c78249 --- /dev/null +++ b/lib/src/scopes/configurables/configurable.dart @@ -0,0 +1,8 @@ +import 'dart:async'; + +import '../scopes/configurable_scope.dart'; + +abstract class Configurable { + FutureOr configure(ConfigurableScope scope); +} + diff --git a/lib/src/scopes/scope_methods/scope_expose.dart b/lib/src/scopes/scope_methods/scope_expose.dart new file mode 100644 index 0000000..4028012 --- /dev/null +++ b/lib/src/scopes/scope_methods/scope_expose.dart @@ -0,0 +1,9 @@ + +import 'package:typedef_foundation/typedef_foundation.dart'; + +abstract class ScopeExpose { + void expose({ + Object? name, + required Getter expose, + }); +} diff --git a/lib/src/scopes/scope_methods/scope_get.dart b/lib/src/scopes/scope_methods/scope_get.dart new file mode 100644 index 0000000..2892bb1 --- /dev/null +++ b/lib/src/scopes/scope_methods/scope_get.dart @@ -0,0 +1,9 @@ + +abstract class ScopeGet { + T? getOrNull({ + Object? name, + }); + bool has({ + Object? name, + }); +} diff --git a/lib/src/scopes/scopes.dart b/lib/src/scopes/scopes.dart new file mode 100644 index 0000000..aadd735 --- /dev/null +++ b/lib/src/scopes/scopes.dart @@ -0,0 +1,6 @@ + +export 'configurables/configurable.dart'; +export 'scope_methods/scope_get.dart'; +export 'scope_methods/scope_expose.dart'; +export 'scopes/configurable_scope.dart'; +export 'scopes/scope.dart'; diff --git a/lib/src/scopes/scopes/configurable_scope.dart b/lib/src/scopes/scopes/configurable_scope.dart new file mode 100644 index 0000000..d319e9c --- /dev/null +++ b/lib/src/scopes/scopes/configurable_scope.dart @@ -0,0 +1,44 @@ +import 'package:meta/meta.dart'; +import 'package:typedef_foundation/typedef_foundation.dart'; + +import '../scope_methods/scope_expose.dart'; +import 'scope.dart'; + +abstract class ConfigurableScope implements Scope, ScopeExpose { + @internal + factory ConfigurableScope() = _ConfigurableScopeImpl; +} + +class _ConfigurableScopeImpl implements ConfigurableScope { + _ConfigurableScopeImpl(): + _storage = {}; + + final Map> _storage; + + @override + T? getOrNull({ + Object? name, + }) { + final getter = _storage[T]?[name] as Getter?; + return getter?.call(); + } + + @override + bool has({ + Object? name, + }) { + return _storage.containsKey(T) && _storage[T]!.containsKey(name); + } + + @override + void expose({ + Object? name, + required Getter expose, + }) { + if (!_storage.containsKey(T)) { + _storage[T] = >{}; + } + _storage[T]![name] = expose; + } +} + diff --git a/lib/src/scopes/scopes/scope.dart b/lib/src/scopes/scopes/scope.dart new file mode 100644 index 0000000..604bee6 --- /dev/null +++ b/lib/src/scopes/scopes/scope.dart @@ -0,0 +1,14 @@ + +import 'dart:async'; + +import '../configurables/configurable.dart'; +import '../scope_methods/scope_get.dart'; +import '../shared/build_scope.dart'; + +import 'configurable_scope.dart'; + +abstract class Scope implements ScopeGet { + static FutureOr root(List configure) => _scopeRoot(configure); +} + +FutureOr _scopeRoot(List configure) => buildScope(configure, ConfigurableScope()); diff --git a/lib/src/scopes/shared/build_scope.dart b/lib/src/scopes/shared/build_scope.dart new file mode 100644 index 0000000..50057ba --- /dev/null +++ b/lib/src/scopes/shared/build_scope.dart @@ -0,0 +1,31 @@ + +import 'dart:async'; +import 'package:meta/meta.dart'; + +import '../configurables/configurable.dart'; +import '../scopes/configurable_scope.dart'; +import '../scopes/scope.dart'; + +@internal +FutureOr buildScope(List configure, ConfigurableScope scope) { + return configure + .fold>(null, (futureOrVoid, configurable) { + return futureOrVoid.then((_) => configurable.configure(scope)); + }) + .then((_) => scope); +} + +extension on FutureOr { + + FutureOr then( + FutureOr Function(T) onValue, { + FutureOr Function(Object error, StackTrace stackTrace)? onError, + }) { + final _this = this; + if (_this is Future) { + return _this.then(onValue, onError: onError); + } else { + return onValue(_this); + } + } +} diff --git a/test/scopes/scopes/scope_test.dart b/test/scopes/scopes/scope_test.dart new file mode 100644 index 0000000..d561b64 --- /dev/null +++ b/test/scopes/scopes/scope_test.dart @@ -0,0 +1,155 @@ +import 'package:test/test.dart'; +import 'package:scopes/scopes.dart'; + +import '../../toolbox/mock_configurable.dart'; + +void main() { + + test('`scope` is `ScopeGet`', () { + + final scope = _MockScope(); + + expect(scope, isA()); + }); + + test('`Scope.root` return sync scope if it only has sync configuration', () { + + final scope = Scope.root([ + MockConfigurable((_) {}), + ]); + + expect(scope, isA()); + }); + + test('`Scope.root` return async scope if it has async configuration', () { + + final scope = Scope.root([ + MockConfigurable((_) async {}), + ]); + + expect(scope, isA>()); + }); + + test('`scope.has` return false if value not exposed', () async { + + final scope = await Scope.root([]); + + final hasValue = scope.has(); + + expect(hasValue, false); + }); + + test('`scope.has` return true if value exposed', () async { + + final scope = await Scope.root([ + MockConfigurable((scope) { + scope.expose(expose: () => 'a'); + }), + ]); + + final hasValue = scope.has(); + + expect(hasValue, true); + }); + + test('`scope.has` return false if value not exposed with name', () async { + + final scope = await Scope.root([]); + + final hasValue = scope.has(name: 'state'); + + expect(hasValue, false); + }); + + test('`scope.has` return true if value exposed with name', () async { + + final scope = await Scope.root([ + MockConfigurable((scope) { + scope.expose( + name: 'state', + expose: () => 'a', + ); + }), + ]); + + final hasValue = scope.has(name: 'state'); + + expect(hasValue, true); + }); + + test('`scope.getOrNull` return null if value not exposed', () async { + + final scope = await Scope.root([]); + + final value = scope.getOrNull(); + + expect(value, null); + }); + + test('`scope.getOrNull` return value if value exposed', () async { + + final scope = await Scope.root([ + MockConfigurable((scope) { + scope.expose(expose: () => 'a'); + }), + ]); + + final value = scope.getOrNull(); + + expect(value, 'a'); + }); + + test('`scope.getOrNull` return null if value not exposed with name', () async { + + final scope = await Scope.root([]); + + final value = scope.getOrNull(name: 'state'); + + expect(value, null); + }); + + test('`scope.getOrNull` return value if value exposed with name', () async { + + final scope = await Scope.root([ + MockConfigurable((scope) { + scope.expose( + name: 'state', + expose: () => 'a', + ); + }), + ]); + + final value = scope.getOrNull(name: 'state'); + + expect(value, 'a'); + }); + + test('`scope.getOrNull` return value if value exposed with name using async configuration', () async { + + final scope = await Scope.root([ + MockConfigurable((scope) async { + scope.expose( + name: 'state', + expose: () => 'a', + ); + }), + ]); + + final value = scope.getOrNull(name: 'state'); + + expect(value, 'a'); + }); +} + +class _MockScope implements Scope { + + @override + T? getOrNull({ + Object? name, + }) => throw UnimplementedError(); + + @override + bool has({ + Object? name, + }) => throw UnimplementedError(); +} diff --git a/test/scopes/scopes_test.dart b/test/scopes/scopes_test.dart new file mode 100644 index 0000000..dd893e4 --- /dev/null +++ b/test/scopes/scopes_test.dart @@ -0,0 +1,6 @@ + +import 'scopes/scope_test.dart' as scope_test; + +void main() { + scope_test.main(); +} diff --git a/test/scopes_test.dart b/test/scopes_test.dart index 59659c1..5cb7253 100644 --- a/test/scopes_test.dart +++ b/test/scopes_test.dart @@ -1,8 +1,10 @@ import 'observables/observables_test.dart' as observalbes_test; +import 'scopes/scopes_test.dart' as scopes_test; void main() { observalbes_test.main(); + scopes_test.main(); } diff --git a/test/toolbox/mock_configurable.dart b/test/toolbox/mock_configurable.dart new file mode 100644 index 0000000..7646e00 --- /dev/null +++ b/test/toolbox/mock_configurable.dart @@ -0,0 +1,13 @@ +import 'dart:async'; +import 'package:scopes/scopes.dart'; + +class MockConfigurable implements Configurable { + MockConfigurable(this._configure); + + final FutureOr Function(ConfigurableScope scope) _configure; + + @override + FutureOr configure(ConfigurableScope scope) { + return _configure(scope); + } +}