aston
v1.0.2
Published
A multi-purpose, immutable, asynchronous, context-safe configuration management utility.
Downloads
6
Readme
Aston JS
A multi-purpose, immutable, asynchronous, context-safe configuration management utility.
Installation
yarn add aston
Usage
Simple
import Aston from 'aston';
const config = Aston.inject({
key: 'SecretKey', value: 1
})
const myContext = config.inject({
key: 'Test', value: 5
})
config.get('SecretKey')
// returns 1
myContext.get('SecretKey')
// returns 1
myContext.get('Test')
// returns 5
config.get('Test')
// throws
Advanced
import Aston from 'aston';
const productionEndPoints = {
cakes: 'sales/cakes',
bread: 'offers/daily/bread'
}
const testEndPoints = {
cakes: 'test/cakes',
bread: 'test/bread'
}
const loadSecretKey = async () => { await ... return secretKey }
const [ENDPOINTS, SECRETKEY] = [Symbol(), Symbol()];
const testConfig = Aston.inject(
{ key: ENDPOINTS, value: testEndPoints },
{ key: SECRETKEY, value: 1 }
)
const prodConfig = Aston.inject(
{ key: ENDPOINTS, value: productionEndpoints },
{ key: SECRETKEY, single: loadSecretKey }
)
class Bakery {
// until we get decorators
static ASTON_INJECT = { endPoints: ENDPOINTS, secretKey: SECRETKEY }
endPoints;
secretKey;
getCakes = () => fetch(this.endPoints.cakes, { auth: this.secretKey });
}
const testEnv = testConfig.inject({ single: Bakery })
const prodEnv = prodConfig.inject({ single: Bakery })
const testBakery = await testEnv.get(Bakery);
testBakery.getCakes();
const prodBakery = await prodEnv.get(Bakery);
prodBakery.getCakes();
Motivation
Configuring a non-trivial application is hard. Some values need to be loaded from the environment, some need to be retrieved from a web service, classes depend on other classes that need to initialize from asynchronous code etc etc.
Declarative and asynchronous
Aston is intended to shift that complexity to a declarative way of specifying dependencies, without needing to explicitly state the initialization order. It will figure out the dependency graph while fetching instances. It will wait for any asynchronous code to complete before continuing, basically giving you the power of having intelligent asynchronous constructors for classes.
Immutable
Aston instances are immutable. That means, once you have constructed a Aston configuration, there is no way to change it. For example:
const config = Aston.inject({ key: 'test', value: 1 });
config.inject({ key: 'bar', value: 123 });
const bar = await config.get('bar');
// throws, since the instance assigned to config does not have knowledge of bar
This keeps behavior predictable. It also means that no dependency can depend on values that are added to a later instance. While perhaps sometimes annoying, this keeps the behavior of instances pure, free of side-effects, and predictable, which is a Good Thing (tm).
Powerful
In Aston world, there is no such thing as a real singleton. A single instance means that, within THAT aston instance and all instances derived from it, the Aston will always return the same instance.
However, nothing is holding you against creating a new Aston instance defining another single instance relying on completely different values for its dependencies.
Huh? No singletons?
That's right, Aston doesn't do anything global, only local within it's own context. So there is no such thing as a 'global' singleton, only a local singleton. This actually allows you to, for example, have multiple active configurations at the same time! (Of course, don't do that if your classes actually use a non-shareable resource)
Details
Aston
The main and default exported value is named Aston
and this is the empty context.
All aston contexts start with this empty context and then inject values
to create a new instance.
inject(...items)
Aston is a key-value store, so each item that is injected needs to have some way to find it back. Keys are unique and cannot be overwritten or removed. In such attemps, inject will throw.
inject({ key, value })
Simply returns a new context with the key set to the specified value and performs no other magic. Get will always retrieve this exact value. Values must always have an explicit key.
inject({ single })
Returns a new context where every time the key is queried from Aston, it will return the same instance. The instance is created the first time it is requested.
single
should be an object, or a creatable value, e.g. a class or function. If
the value is a function returning a promise, it will resolve this promise and
pass the resulting value when queried through get
or getItems
.
The following extra properties can optionally be added:
key
: specifies the key for which the instance can be retrieved, can be any typeargs
: specifies an array of arguments to be passed to the creation function or class constructorsupplyAston
: when true andsingle
is a function, it's owning Aston instance will be passed as first parameter
If the value is an object or class, and no key is specified, Aston will assume the class constructor is the key.
const config = Aston.inject(
{ single: BarClass, args: [1, 'Test'] },
{ key: 'foo',
single: async aston => new FooClass(await aston.get(BarClass),
supplyAston: true
}
);
const foo = await config.get('foo');
inject({ multi })
Returns a new context where every time the key is queried from Aston, it will return a new instance.
multi
should be a creatable value, e.g. a class or function. If
the value is a function returning a promise, it will resolve this promise and
pass the resulting value when queried through get
or getItems
.
The following extra properties can optionally be added:
key
: specifies the key for which the instance can be retrieved, can be any typeargs
: specifies an array of arguments to be passed to the creation function or class constructorsupplyAston
: when true andmulti
is a function, it's owning Aston instance will be passed as first parameter
If the value is a class, and no key is specified, Aston will assume the key is the class constructor.
const config = Aston.inject(
{ multi: BarClass, args: [1, 'Test'] },
{ key: 'foo',
single: (aston, a, b) => new FooClass(a, b, aston.get(BarClass)),
supplyAston: true
}
)
const foo = await config.get('foo', 'valueA', 3.14);
get(key, ...args)
(promise)
Retrieves a dependency from the aston instance using the given key. If extra arguments
are given, these will be passed to the function or constructor of a multi
entry.
Note that passing extra arguments to an entry that is single
or value
will throw.
getItems(...keys)
(promise)
Returns an array of the items corresponding to the provided keys. If arguments need to be passed, the key can be replaced by an array starting with the key and then the arguments.
const config = Aston.inject(....);
const [item1, item2, item3, item4] =
await config.getItems('key1', 'key2', ['key3', 3.14, 'Test'], 'key4')
Class-based injection
When using classes it is also possible, when the classes are created through Aston, to specify dependencies using static configuration options. Unfortunately, decorators are not yet available in most JS environments.
Note that these only work if the class is directly constructed by Aston!
ASTON_CONSTRUCTOR
Using the ASTON_CONSTRUCTOR
static value on a class, an array of keys can be
specified corresponding to Aston instances. Aston will then supply these instances
to the class constructor upon creation.
class Foo {
static ASTON_CONSTRUCTOR = [Bar, 'item']
constructor(bar, item) {
...
}
}
const config = Aston.inject(
{ single: class Bar {} },
{ key: 'item', value: 1 },
{ single: Foo }
)
const foo = await config.get(Foo);
ASTON_INJECT
Using the ASTON_INJECT
static value on a class, an object can be specified that
maps class properties to Aston keys. Aston will then set the class properties to
the instances corresponding to the given keys.
Note that this happens after construction, so in the constructor no access is possible to these values.
class Foo {
static ASTON_INJECT = { bar: Bar, test: 'item' }
bar;
test;
action = () => {
this.bar.method(this.test.data);
}
}
const config = Aston.inject(
{ single: class Bar { method () {} } },
{ key: 'item', value: { data: 1 } },
{ single: Foo }
)
const foo = await config.get(Foo);
Related
tbd