Skip to content
This repository was archived by the owner on Sep 17, 2023. It is now read-only.

Commit 9987443

Browse files
committed
Add documentation
1 parent ed26e91 commit 9987443

File tree

5 files changed

+299
-28
lines changed

5 files changed

+299
-28
lines changed

README.md

Lines changed: 272 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
IoC context values provider
1+
IoC Context Values Provider
22
===========================
33

4+
This library allows to construct an [IoC] context, other components can request values from.
5+
6+
An [IoC] context is an object with `get()` method implemented. This method returns a context value by its key.
7+
48
[![NPM][npm-image]][npm-url]
59
[![CircleCI][ci-image]][ci-url]
610
[![codecov][codecov-image]][codecov-url]
@@ -11,3 +15,270 @@ IoC context values provider
1115
[ci-url]:https://circleci.com/gh/surol/context-values
1216
[codecov-image]: https://codecov.io/gh/surol/context-values/branch/master/graph/badge.svg
1317
[codecov-url]: https://codecov.io/gh/surol/context-values
18+
[IoC]: https://en.wikipedia.org/wiki/Inversion_of_control
19+
20+
21+
Accessing Context Values
22+
------------------------
23+
24+
A context should implement a `ContextValues` interface. This interface declares a `get()` method accepting a
25+
`ContextRequest` identifying the requested value (e.g. a `ContextKey` instance), and a non-mandatory options.
26+
27+
The following code returns a string value associated with `key`, or throws an exception if the value not found.
28+
```typescript
29+
import { SingleContextKey } from 'context-values';
30+
31+
const key = new SingleContextKey<string>('my-key');
32+
33+
myContext.get(key)
34+
```
35+
36+
### Fallback Value
37+
38+
Normally, if the value associated with the given key can not be found, an exception is thrown. To avoid this, a fallback
39+
value can be provided. It will be returned if the value not found.
40+
```typescript
41+
import { SingleContextKey } from 'context-values';
42+
43+
const key = new SingleContextKey<string>('my-key');
44+
45+
myContext.get(key, { or: 'empty' });
46+
```
47+
48+
49+
### Context Value Request
50+
51+
The `get()` method accepts not only a `ContextKey` instances, but arbitrary `ContextRequest`. The latter is just an
52+
object with `key` property containing a `ContextKey` instance to find.
53+
54+
This can be handy e.g. when requesting an instance of some known type:
55+
```typescript
56+
import { ContextKey, SingleContextKey } from 'context-values';
57+
58+
class MyService {
59+
60+
// MyService class (not instance) implements a `ContextRequest`
61+
static readonly key: ContextKey<MyService> = new SingleContextKey('my-service');
62+
63+
}
64+
65+
myContext.get(MyService); // No need to specify `MyService.key` here
66+
myContext.get(MyService.key); // The same as above.
67+
```
68+
69+
70+
Providing Context Values
71+
------------------------
72+
73+
Context values can be provided using `ContextRegistry`.
74+
Then the values can be requested from `ContextValues` instance constructed by the `newValues()` method of the registry.
75+
76+
```typescript
77+
import { ContextRegistry, SingleContextKey } from 'context-values';
78+
79+
const key1 = new SingleContextKey<string>('key1');
80+
const key2 = new SingleContextKey<number>('key2');
81+
82+
const registry = new ContextRegistry();
83+
84+
registry.provide({ a: key1, is: 'string' });
85+
registry.provide({ a: key2, by: ctx => ctx.get(key1).length })
86+
87+
const context = registry.newValues();
88+
89+
context.get(key1); // 'string'
90+
context.get(key2); // 6
91+
```
92+
93+
94+
### Context Value Target
95+
96+
[Context Value Target]: #context-value-target
97+
98+
The `provide()` method accepts not only a `ContextKey` instances, but arbitrary `ContextTarget`. The latter is just an
99+
object with `key` property containing a `ContextKey` to provide.
100+
101+
102+
This can be handy e.g. when providing an instance of some known type:
103+
```typescript
104+
import { ContextKey, ContextRegistry, SingleContextKey } from 'context-values';
105+
106+
class MyService {
107+
108+
// MyService class (not instance) implements a `ContextRequest`
109+
static readonly key: ContextKey<MyService> = new SingleContextKey('my-service');
110+
111+
}
112+
113+
const registry = new ContextRegistry();
114+
115+
registry.provide({ a: MyService, is: new MyService() });
116+
117+
const context = registry.newValues();
118+
119+
context.get(MyService); // No need to specify `MyService.key` here
120+
```
121+
122+
123+
### Context Value Specifier
124+
125+
The `provide()` method of `ContextRegistry` accepts a _context value specifier_ as its only parameter.
126+
127+
This specifier defines a value (or, more precisely, the [value sources]). It may specify the value in a different ways:
128+
129+
- `registry.provide({ a: key, is: value })` - provides the value explicitly.
130+
- `registry.provide({ a: key, by: ctx => calculateValue(ctx) })` - evaluates the value in most generic way. `ctx` here
131+
is the target context.
132+
- `registry.provide({ a: key, by: (a, b) => calculateValue(a, b), with: [keyA, keyB] })` - evaluates the value based on
133+
other context values with keys `keyA` and `keyB`.
134+
- `registry.provide({ a: key, as: MyService })` - constructs the value a `new MyService(ctx)`, where `ctx` is the
135+
target context. The `a` property may be omitted if `MyService` has a static property `key`.
136+
See [Context Value Target].
137+
- `registry.porvide({ a: key, as: MyService, with: [keyA, keyB] })` - constructs the value as `new MyService(a, b)`,
138+
where `a` and `b` are context values with keys `keyA` and `keyB` respectively. The `a` property may be omitted if
139+
`MyService` has a static property `key`. See [Context Value Target].
140+
141+
142+
Context Value Key
143+
-----------------
144+
145+
Context value keys identify context values.
146+
147+
They extend a `ContextKey` abstract class. There following implementations are available:
148+
149+
- `SingleContextKey` that allows associate a single value with it, and
150+
- `MultiContextKey` that allows to associate multiple values with it.
151+
152+
```typescript
153+
import { ContextRegistry, SingleContextKey, MultiContextKey } from 'context-values';
154+
155+
const key1 = new SingleContextKey<string>('key1');
156+
const key2 = new MultiContextKey<number>('key2');
157+
158+
const registry = new ContextRegistry();
159+
160+
registry.provide({ a: key1, is: 'value1' });
161+
registry.provide({ a: key1, is: 'value2' });
162+
registry.provide({ a: key2, is: 1 });
163+
registry.provide({ a: key2, is: 2 });
164+
165+
const context = registry.newValues();
166+
167+
context.get(key1); // 'value2' - SingleContextKey uses the latest value provided
168+
context.get(key2); // [1, 2] - MultiContextKey returns all provided values as an array
169+
```
170+
171+
### Default Value
172+
173+
Context value key may declare a default value. It will be evaluated and returned when the value is not found and no
174+
fallback value specified in the request.
175+
176+
The default value is evaluated by the function accepting a `ContextValues` instance as its only argument.
177+
```typescript
178+
import { ContextRegistry, SingleContextKey, MultiContextKey } from 'context-values';
179+
180+
const key1 = new SingleContextKey<string>('key1');
181+
const key2 = new SingleContextKey<number>('key2', ctx => ctx.get('key1').length);
182+
const key3 = new MultiContextKey<number>('key3');
183+
184+
const registry = new ContextRegistry();
185+
186+
registry.provide({ a: key1, is: 'value' });
187+
188+
const context = registry.newValues();
189+
190+
context.get(key1); // 'value'
191+
context.get(key2); // 6 - evaluated, as it is not provided
192+
context.get(key2, { or: null }); // null - fallback value always takes precedence
193+
context.get(key3); // [] - MultiContextKey uses it as a default value, unless explicitly specified
194+
195+
registry.provide({ a: key2, value: 999 });
196+
197+
context.get(key2); // 999 - provided explicitly
198+
```
199+
200+
### Custom Context Key
201+
202+
[ContextKey.merge()]: #custom-context-key
203+
204+
It is possible to implement a custom `ContextKey`.
205+
206+
For that extend an `AbstractContextKey` that implements the boilerplate. The only method left to implement then is a
207+
`merge()` one.
208+
209+
The `merge()` method takes three parameters:
210+
- a `ContextValues` instance (to consult other context values if necessary),
211+
- a `ContextSources` instance (containing provided [value sources]), and
212+
- a `handleDefault` function responsible for the default value selection.
213+
214+
The method returns a context value constructed out of the provided value sources.
215+
216+
217+
#### Value Sources
218+
219+
[value sources]: #value-sources
220+
221+
Instead of the values themselves, the registry allows to provide value sources. Those are used by [ContextKey.merge()]
222+
method to construct the value.
223+
224+
There could be many sources per single value. And they could be of a type different from the final value.
225+
226+
The sources are passed to the `merge()` function as an [AIterable] instance. The latter is an enhanced `Iterable` with
227+
Array-like API, including `map()`, `flatMap()`, `forEach()`, and other methods.
228+
229+
[AIterable]: https://www.npmjs.com/package/a-iterable
230+
231+
```typescript
232+
import {
233+
AbstractContextKey,
234+
ContextRegistry,
235+
ContextSources,
236+
ContextValues,
237+
Handler,
238+
DefaultContextValueHandler
239+
} from 'context-values';
240+
241+
class ConcatContextKey<V> extends AbstractContextKey<V, string> {
242+
243+
constructor(name: string) {
244+
super(name);
245+
}
246+
247+
merge(
248+
context: ContextValues,
249+
sources: ContextSources<string>,
250+
handleDefault: DefaultContextValueHandler<string>): string | null | undefined {
251+
252+
const result = sources.reduce((p, s) => p != null ? `${p}, ${s}` : `${s}`, null);
253+
254+
if (result != null) {
255+
return result;
256+
}
257+
258+
return handleDefault(() => ''); // No sources provided. Returning empty string, unless a fallback value provided.
259+
}
260+
261+
}
262+
263+
const key1 = new ConcatContextKey<number>('my-numbers');
264+
const key2 = new ConcatContextKey<string>('my-string');
265+
266+
const registry = new ContextRegistry();
267+
268+
registry.provide({ a: key1, is: 1 });
269+
registry.provide({ a: key1, is: 2 });
270+
registry.provide({ a: key1, is: 3 });
271+
272+
const context = registry.newValues();
273+
274+
context.get(key1); // '1, 2, 3' - concatenated value
275+
context.get(key2); // '' - empty string by default
276+
context.get(key2, { or: undefined }); // undefined - fallback value
277+
```
278+
279+
A context value for particular key is constructed at most once. Thus, the `merge()` method is called at most once per
280+
key.
281+
282+
A [context value specifier](#context-value-specifier) is consulted at most once per key. And only when the `merge()`
283+
method requested the source value. So, for example, if multiple sources specified for the same `SingleContextKey`, only
284+
the last one will be constructed and used as a context value. The rest of them won't be constructed at all.

src/context-registry.spec.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,18 @@ describe('ContextRegistry', () => {
4141

4242
expect(values.get(keyWithDefaults)).toBe(defaultValue);
4343
});
44-
it('is associated with default value is there is no provider', () => {
44+
it('is associated with default value if there is no provider', () => {
4545
expect(values.get(new SingleContextKey<string>(key.name, () => 'default'))).toBe('default');
4646
});
47-
it('prefers explicit default value over key one', () => {
47+
it('prefers fallback value over default one', () => {
4848
expect(values.get(new SingleContextKey<string>(key.name, () => 'key default'), { or: 'explicit default' }))
4949
.toBe('explicit default');
5050
});
51-
it('prefers explicit `null` default value over key one', () => {
51+
it('prefers `null` fallback value over key one', () => {
5252
expect(values.get(new SingleContextKey<string>(key.name, () => 'default'), { or: null }))
5353
.toBeNull();
5454
});
55-
it('prefers explicit `undefined` default value over key one', () => {
55+
it('prefers `undefined` fallback value over key one', () => {
5656
expect(values.get(new SingleContextKey<string>(key.name, () => 'default'), { or: undefined }))
5757
.toBeUndefined();
5858
});
@@ -81,7 +81,7 @@ describe('ContextRegistry', () => {
8181

8282
expect(providerSpy).toHaveBeenCalledTimes(1);
8383
});
84-
it('caches default key value', () => {
84+
it('caches default value', () => {
8585

8686
const value = 'default value';
8787
const defaultProviderSpy = jest.fn(() => value);
@@ -91,7 +91,7 @@ describe('ContextRegistry', () => {
9191
expect(values.get(keyWithDefault)).toBe(value);
9292
expect(defaultProviderSpy).toHaveBeenCalledTimes(1);
9393
});
94-
it('does not cache explicit default value', () => {
94+
it('does not cache fallback value', () => {
9595

9696
const value1 = 'value1';
9797
const value2 = 'value2';
@@ -159,19 +159,19 @@ describe('ContextRegistry', () => {
159159
it('is associated with key default value is there is no value', () => {
160160
expect(values.get(new MultiContextKey<string>(multiKey.name, () => ['default']))).toEqual(['default']);
161161
});
162-
it('prefers explicit default value over key one', () => {
162+
it('prefers fallback value over default one', () => {
163163
expect(values.get(
164164
new MultiContextKey<string>(
165165
multiKey.name,
166166
() => ['key', 'default']),
167167
{ or: ['explicit', 'default'] }))
168168
.toEqual(['explicit', 'default']);
169169
});
170-
it('prefers explicit `null` default value over key one', () => {
170+
it('prefers `null` fallback value over default one', () => {
171171
expect(values.get(new MultiContextKey<string>(multiKey.name, () => ['key', 'default']), { or: null }))
172172
.toBeNull();
173173
});
174-
it('prefers explicit `undefined` default value over key one', () => {
174+
it('prefers `undefined` fallback value over default one', () => {
175175
expect(values.get(new MultiContextKey<string>(multiKey.name, () => ['key', 'default']), { or: undefined }))
176176
.toBeUndefined();
177177
});

src/context-registry.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
ContextSources,
66
ContextSourcesKey,
77
ContextTarget,
8-
ContextValueDefaultHandler,
8+
DefaultContextValueHandler,
99
} from './context-value';
1010
import { ContextSourcesProvider, ContextValueProvider, ContextValueSpec } from './context-value-provider';
1111
import { ContextValues } from './context-values';
@@ -15,7 +15,7 @@ import { ContextValues } from './context-values';
1515
*
1616
* @param <C> A type of context.
1717
*/
18-
export class ContextRegistry<C extends ContextValues> {
18+
export class ContextRegistry<C extends ContextValues = ContextValues> {
1919

2020
/** @internal */
2121
private readonly _initial: ContextSourcesProvider<C>;
@@ -142,7 +142,7 @@ export class ContextRegistry<C extends ContextValues> {
142142
}
143143

144144
let defaultUsed = false;
145-
const handleDefault: ContextValueDefaultHandler<V> = opts
145+
const handleDefault: DefaultContextValueHandler<V> = opts
146146
? () => {
147147
defaultUsed = true;
148148
return opts.or;

0 commit comments

Comments
 (0)