feat: add package:scoped (#600)
This commit is contained in:
@@ -66,6 +66,10 @@ jobs:
|
||||
- ./.github/workflows/main.yaml
|
||||
- ./.github/actions/dart_package
|
||||
- packages/jwt/**
|
||||
scoped:
|
||||
- ./.github/workflows/main.yaml
|
||||
- ./.github/actions/dart_package
|
||||
- packages/scoped/**
|
||||
|
||||
- uses: dorny/paths-filter@v2
|
||||
name: Verify Detection
|
||||
|
||||
@@ -11,7 +11,7 @@ Home of the Shorebird Tools
|
||||
|
||||
## Status
|
||||
|
||||
Shorebird code push is in Open Beta! Instructions for install and usage are at
|
||||
Shorebird code push is in Open Beta! Instructions for install and usage are at
|
||||
https://docs.shorebird.dev/
|
||||
|
||||
## Packages
|
||||
@@ -25,6 +25,7 @@ This repository is a monorepo containing the following packages:
|
||||
| [shorebird_code_push_protocol](packages/shorebird_code_push_protocol/README.md) | Dart library which contains common interfaces used by Shorebird CodePush |
|
||||
| [discord_gcp_alerts](packages/discord_gcp_alerts/README.md) | Dart server which forwards GCP alerts to Discord |
|
||||
| [jwt](packages/jwt/README.md) | Dart library for verifying Json Web Tokens |
|
||||
| [scoped](packages/scoped/README.md) | A simple dependency injection library built on Zones |
|
||||
|
||||
For more information, please refer to the documentation for each package.
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# See https://www.dartlang.org/guides/libraries/private-files
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
pubspec.lock
|
||||
@@ -0,0 +1,24 @@
|
||||
# Scoped
|
||||
|
||||
A simple dependency injection library built on Zones.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```dart
|
||||
import 'package:scoped/scoped.dart';
|
||||
|
||||
final value = create(() => 42);
|
||||
|
||||
void main() {
|
||||
runScoped(scopeA, values: {value});
|
||||
}
|
||||
|
||||
void scopeA() {
|
||||
print(read(value)); // 42
|
||||
runScoped(scopeB, values: {value.overrideWith(() => 0)});
|
||||
}
|
||||
|
||||
void scopeB() {
|
||||
print(read(value)); // 0
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
include: package:very_good_analysis/analysis_options.5.0.0.yaml
|
||||
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="102" height="20">
|
||||
<linearGradient id="b" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1" />
|
||||
<stop offset="1" stop-opacity=".1" />
|
||||
</linearGradient>
|
||||
<clipPath id="a">
|
||||
<rect width="102" height="20" rx="3" fill="#fff" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#a)">
|
||||
<path fill="#555" d="M0 0h59v20H0z" />
|
||||
<path fill="#44cc11" d="M59 0h43v20H59z" />
|
||||
<path fill="url(#b)" d="M0 0h102v20H0z" />
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110">
|
||||
<text x="305" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="490">coverage</text>
|
||||
<text x="305" y="140" transform="scale(.1)" textLength="490">coverage</text>
|
||||
<text x="795" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="330">100%</text>
|
||||
<text x="795" y="140" transform="scale(.1)" textLength="330">100%</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,17 @@
|
||||
// ignore_for_file: avoid_print
|
||||
import 'package:scoped/scoped.dart';
|
||||
|
||||
final value = create(() => 42);
|
||||
|
||||
void main() {
|
||||
runScoped(scopeA, values: {value});
|
||||
}
|
||||
|
||||
void scopeA() {
|
||||
print(read(value)); // 42
|
||||
runScoped(scopeB, values: {value.overrideWith(() => 0)});
|
||||
}
|
||||
|
||||
void scopeB() {
|
||||
print(read(value)); // 0
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/// A simple dependency injection library built on Zones
|
||||
library scoped;
|
||||
|
||||
export 'src/scoped.dart';
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// {@template scoped_ref}
|
||||
/// A reference to a scoped value.
|
||||
/// {@endtemplate}
|
||||
@immutable
|
||||
class ScopedRef<T> {
|
||||
/// {@macro scoped_ref}
|
||||
ScopedRef(this._create) : _key = Object();
|
||||
|
||||
ScopedRef._(T Function() create, Object key)
|
||||
: _create = create,
|
||||
_key = key;
|
||||
|
||||
final T Function() _create;
|
||||
final Object _key;
|
||||
late final T _value = _create();
|
||||
|
||||
/// Overrides the value of the current [ScopedRef]
|
||||
/// with the provided [create].
|
||||
ScopedRef<T> overrideWith(T Function() create) {
|
||||
return ScopedRef<T>._(create, _key);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (runtimeType != other.runtimeType) return false;
|
||||
if (other is ScopedRef<T>) return _key == other._key;
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => _key.hashCode;
|
||||
}
|
||||
|
||||
/// Creates a [ScopedRef] which can later be used to access the
|
||||
/// value, [T] returned by [create].
|
||||
ScopedRef<T> create<T>(T Function() create) => ScopedRef<T>(create);
|
||||
|
||||
/// Attempts to retrieve the value for the [ref].
|
||||
/// If [read] is called with a [ref] which is not available
|
||||
/// in the current scope, a [StateError] will be thrown.
|
||||
T read<T>(ScopedRef<T> ref) {
|
||||
final value = (Zone.current[ref._key] as ScopedRef<T>?)?._value;
|
||||
if (value == null) {
|
||||
throw StateError(
|
||||
'''
|
||||
read(...) was called in a scope which does not contain a corresponding value for the provided ref.
|
||||
Did you forget to call: runScoped(() {...}, values: {value})?''',
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Runs [body] within a scope which has access to the set of refs in [values].
|
||||
R runScoped<R>(
|
||||
R Function() body, {
|
||||
Set<ScopedRef<dynamic>> values = const {},
|
||||
}) {
|
||||
return runZoned(
|
||||
body,
|
||||
zoneValues: {for (final value in values) value._key: value},
|
||||
);
|
||||
}
|
||||
|
||||
/// Runs [body] within a scope which has access to the set of refs in [values].
|
||||
R? runScopedGuarded<R>(
|
||||
R Function() body, {
|
||||
required void Function(Object error, StackTrace stack) onError,
|
||||
Set<ScopedRef<dynamic>> values = const {},
|
||||
}) {
|
||||
return runZonedGuarded(
|
||||
body,
|
||||
onError,
|
||||
zoneValues: {for (final value in values) value._key: value},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
name: scoped
|
||||
description: A simple dependency injection library built on Zones
|
||||
version: 0.1.0+1
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
||||
dependencies:
|
||||
meta: ^1.0.0
|
||||
|
||||
dev_dependencies:
|
||||
mocktail: ^0.3.0
|
||||
test: ^1.0.0
|
||||
very_good_analysis: ^5.0.0
|
||||
@@ -0,0 +1,86 @@
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
import 'package:scoped/scoped.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('Scoped', () {
|
||||
test('read throws StateError when ref is not available', () {
|
||||
final value = create(() => 42);
|
||||
expect(() => read(value), throwsStateError);
|
||||
});
|
||||
|
||||
test('calls onError when uncaught exception occurs', () {
|
||||
final value = create(() => 42);
|
||||
late final Object exception;
|
||||
runScopedGuarded(
|
||||
() => read(value),
|
||||
onError: (error, _) => exception = error,
|
||||
);
|
||||
expect(exception, isNotNull);
|
||||
});
|
||||
|
||||
test('read accesses the value when ref is available', () {
|
||||
final value = create(() => 42);
|
||||
runScoped(
|
||||
() => expect(read(value), equals(42)),
|
||||
values: {value},
|
||||
);
|
||||
});
|
||||
|
||||
test('value is computed lazily and cached', () {
|
||||
var createCallCount = 0;
|
||||
final value = create(() {
|
||||
createCallCount++;
|
||||
return 42;
|
||||
});
|
||||
|
||||
expect(createCallCount, equals(0));
|
||||
|
||||
runScoped(
|
||||
() {
|
||||
expect(read(value), equals(42));
|
||||
expect(read(value), equals(42));
|
||||
expect(read(value), equals(42));
|
||||
},
|
||||
values: {value},
|
||||
);
|
||||
|
||||
expect(createCallCount, equals(1));
|
||||
});
|
||||
|
||||
test('value can be overridden', () {
|
||||
final value = create(() => 42);
|
||||
|
||||
runScoped(
|
||||
() {
|
||||
expect(read(value), equals(42));
|
||||
|
||||
runScoped(
|
||||
() => expect(read(value), equals(0)),
|
||||
values: {value.overrideWith(() => 0)},
|
||||
);
|
||||
},
|
||||
values: {value},
|
||||
);
|
||||
});
|
||||
|
||||
test('overrides are considered equal', () {
|
||||
final value = create(() => 42);
|
||||
final override = value.overrideWith(() => 0);
|
||||
expect(value, equals(override));
|
||||
expect(value.hashCode, equals(override.hashCode));
|
||||
});
|
||||
|
||||
test('same instance is equal', () {
|
||||
final value = create(() => 42);
|
||||
expect(value, equals(value));
|
||||
});
|
||||
|
||||
test('different instances are not equal', () {
|
||||
final valueA = create(() => 42);
|
||||
final valueB = create(() => 42);
|
||||
expect(valueA, isNot(equals(valueB)));
|
||||
expect(valueA.hashCode, isNot(equals(valueB.hashCode)));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user