feat: add package:scoped (#600)

This commit is contained in:
Felix Angelov
2023-06-07 08:19:34 -07:00
committed by GitHub
parent c1c87effb9
commit f3da091bb0
11 changed files with 260 additions and 1 deletions
+4
View File
@@ -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
+2 -1
View File
@@ -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.
+7
View File
@@ -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
+24
View File
@@ -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
}
```
+1
View File
@@ -0,0 +1 @@
include: package:very_good_analysis/analysis_options.5.0.0.yaml
+20
View File
@@ -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

+17
View File
@@ -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
}
+4
View File
@@ -0,0 +1,4 @@
/// A simple dependency injection library built on Zones
library scoped;
export 'src/scoped.dart';
+80
View File
@@ -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},
);
}
+15
View File
@@ -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
+86
View File
@@ -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)));
});
});
}