11. Extended Fetaures
11.1. Array and Object Destructuring
N4JS supports array and object destructuring as provided in ES6. This is used to conveniently assign selected elements of an array or object to a number of newly-declared or pre-existing variables or to further destructure them by using nested destructuring patterns [55].
11.1.1. Syntax
BindingPattern <Yield>:
ObjectBindingPattern<Yield>
| ArrayBindingPattern<Yield>
;
ObjectBindingPattern <Yield> returns BindingPattern:
{BindingPattern}
'{' (properties+=BindingProperty<Yield,AllowType=false> (',' properties+=BindingProperty<Yield,AllowType=false>)*)? '}'
;
ArrayBindingPattern <Yield> returns BindingPattern:
{BindingPattern}
'['
elements+=Elision* (
elements+=BindingRestElement<Yield>
(',' elements+=Elision* elements+=BindingRestElement<Yield>)*
(',' elements+=Elision*)?
)?
']'
;
BindingProperty <Yield, AllowType>:
=>(LiteralBindingPropertyName<Yield> ':') value=BindingElement<Yield>
| value=SingleNameBinding<Yield,AllowType>
;
fragment LiteralBindingPropertyName <Yield>*:
declaredName=IdentifierName | declaredName=STRING | declaredName=NumericLiteralAsString
// this is added here due to special treatment for a known set of expressions
| '[' (declaredName=SymbolLiteralComputedName<Yield> | declaredName=STRING) ']'
;
11.1.2. Semantics
The following example declares four variables a
, b
, x
, and prop2
. Variables a
and x
will have the value hello
, whereas b
and prop2
will have value 42.
var [a,b] = ["hello", 42];
var {prop1:x, prop2} = {prop1:"hello", prop2:42};
In the case of prop2
, we do not provide a property name and variable name separately; this is useful in cases where the property name also makes for a
suitable variable name (called single name binding
).
One of the most useful use cases of destructuring is in a for..of
loop.
Take this example:
var arr1 = [ ["hello",1,2,3], ["goodbye",4,5,6] ];
for(var [head,...tail] of arr1) {
console.log(head,'/',tail);
}
// will print:
// hello / [ 1, 2, 3 ]
// goodbye / [ 4, 5, 6 ]
var arr2 = [ {key:"hello", value:42}, {key:"goodbye", value:43} ];
for(var {key,value} of arr2) {
console.log(key,'/',value);
}
// will print:
// hello / 42
// goodbye / 43
Array and object destructuring pattern can appear in many different places:
-
In a variable declaration (not just in variable statements but also in other places where variable declarations are allowed, e.g. plain for loops; called destructuring binding; see Variable Statement).
-
On the left-hand side of an assignment expression (the assignment expression is then called destructuring assignment; see Assignment Expression).
-
In a
for..in
orfor..of
loop on the left side of thein
/of
(seefor … of
statement).It can also be used in plain statements, but then we actually have one of the above two use cases. -
With lists of formal parameters or function arguments (not supported yet).
For further details on array and object destructuring please refer to the ECMAScript 6 specification - [ECMA15a].
Type annotations can only be added when a new variable name is introduced since the short version would be ambiguous with the long one. For example:
var {x: someTypeOrNewVar} = ol
could either mean that a new variable someTypeOrNewVar
is declared and ol.x
is assigned to it, or that a new variable x
is declared with type someTypeOrNewVar
.
The longer form would look like this:
var {x: x: someType} = ol
We can make this more readable:
var {propOfOl: newVar: typeOfNewVar} = ol
11.2. Dependency Injection
This chapter describes DI mechanisms for N4JS. This includes compiler, validation and language extensions that allow to achieve DI mechanisms built in into the N4JS language and IDE.
N4JS DI support specifies a means for obtaining objects in such a way as to maximize reusability, testability and maintainability, especially compared to traditional approaches such as constructors, factories and service locators. While this can be achieved manually (without tooling support) it is difficult for nontrivial applications. The solutions that DI provides should empower N4JS users to achieve the above goals without the burden of maintaining so-called ’boilerplate’ code.
key: pass the dependency instead of letting the client create or find it
Core terms
-
Service - A set of APIs describing the functionality of the service.
-
Service Implementations - One or more implementations of given service API.
-
Client - Consumer of a given functionality, uses the given Service Implementation.
-
Injector - Object providing Service Implementation of a specific Service, according to configuration.
-
Binding - Part of configuration describing which interface implementing a subtype will be injected, when a given interface is requested.
-
Provider - Factory used to create instances of a given Service Implementation or its sub-components, can be a method.
-
Injection Point - Part of the user’s code that will have the given dependency injected. This is usually fields, method parameters, constructor parameters etc.
-
DI configuration - This describes which elements of the user’s code are used in mechanisms and how they are wired. It is derived from user code elements being marked with appropriate annotations, bindings and providers.
-
di wiring - The code responsible for creating user objects. These are injectors, type factories/providers, fields initiators etc.
11.2.1. DI Components and Injectors
N4JS’ Dependency Injection systems is based on the notion of DIC.
Definition: DI Component
A DIC is a N4Class annotated with @GenerateInjector
.
This annotation causes an injector to be created for (and associated to) the DI.
DIC can be composed; meaning that when requested to inject an instance of a type, a DIC’s injector can delegate this request to the injector of the containing DIC.
An injector always prioritizes its own configuration before delegating to the container’s injector.
For validation purposes, a child DI can be annotated with @WithParent
to ensure that it is always used with a proper parent.
Injector is the main object of DI mechanisms responsible for creating object graphs of the application.
At runtime, injectors are instances of N4Injector
.
Req. IDE-138: DI Component and Injector (ver. 1)
The following constraints must hold for a class marked as DIC:
-
A subclass of is a DIC as well and it must be marked with
GenerateInjector
. -
If a parent DIC is specified via
WithParent
, then must be a DIC as well. -
The injector associated to a DIC is of type
N4Injector
. It can be retrieved viaN4Injector.of(DIC)
in whichDIC
is theDIC
. -
Injectors associated to DIC a are DI-singletons (cf. Singleton Scope). Two calls to
N4Injector.of(DIC)
are different (as different DIC are assumed).
Req. IDE-139: Injection Phase (ver. 1)
We call the (transitive) creation and setting of values by an injector caused by the creation of an root object the injection phase. If an instance is newly created by the injector (regardless of the injection point being used), the injection is transitively applied on . The following constraints have to hold:
-
Root objects are created by one of the following mechanisms:
-
Any class or interface can be created as root objects via an injector associated to a DIC:
var x: X = N4Injector.of(DIC).create(X);
in whichDIC
is a DIC.Of course, an appropriate binding must exist. [56]
-
If a type has the injector being injected, e.g. via field injection
@Inject injector: N4Injector;
, then this injector can be used anytime in the control flow to create a new root object similar as above (usingcreate
method). -
If a provider has been injected (i.e. an instance of
{N4Provider}
), then itsget()
method can be used to create a root object causing a new injection phase to take place.
-
-
If is marked as injection point, all its arguments are set by the injector. This is also true for an inherited constructor marked as an injection point. See [Req-IDE-143] . For all arguments the injection phase constraints have to hold as well.
-
All fields of , including inherited once, marked as injection points are set by the injector. For all fields the injection phase constraints have to hold as well.
The injector may use a provider method (of a binder) to create nested instances.
The injector is configured with Binders and it tracks Bindings between types (Binders and Bindings).
An N4JS developer normally would not interact with this object directly except when defining an entry-point to his application.
Injectors are configured with Binders which contain explicit Bindings defined by an N4JS developer.
A set of these combined with implicit bindings creates the di configuration used by a given injector.
To configure given Injectors with given Binder(s) use @UseBinder
annotation.
11.2.1.1. DIComponent Relations
A Parent-Child relation can be established between two DIComponents.
Child DIComponents use the parent bindings but can also be configured with their own bindings or change targets used by a parent.
The final circumstance is local to the child and is referred to as rebinding.
For more information about bindings see Binders and Bindings.
A Child-Parent relation is expressed by the @WithParentInjector
annotation attached to a given DIComponent.
When this relation is defined between DIComponents, the user needs to take care to preserve the proper relation between injectors.
In other words, the user must provide an instance of the parent injector (the injector of the DIComponent passes as a parameter to @WithParentInjector
) when creating the child injector
(injector of the DIComponent annotated with @WithParentInjector
).
@GenerateInjector
class ParentDIComponent{}
@GenerateInjector
@WithParentInjector(ParentDIComponent)
class ChildDIComponent{}
var parentInejctor = N4Inejctor.of(ParentDiCompoennt);
var childInjector = N4Inejctor.of(ChildDIComponent, parentInjector);
With complex DIComponent structures, injector instances can be created with a directly-declared parent and also with any of its children. This is due to the fact that any child can rebind types, add new bindings, but not remove them. Any child is, therefore, compatible with its parents.
Definition: Compatible DIComponent
A given DIComponent is compatible with another DIComponent if it has bindings for all keys in other component bindings.
Although subtype notation is used here it does not imply actual subtype relations. It was used in this instance for of lack of formal notations for DI concepts and because this is similar to the Liskov Substitution principle. |
A complex Child-Parent relation between components is depicted in Complex DIComponents Relations and Complex DIComponents Relations below.
@GenerateInjector class A {}
@GenerateInjector @WithParentInjector(A) class B {}
@GenerateInjector @WithParentInjector(B) class C {}
@GenerateInjector @WithParentInjector(C) class D {}
@GenerateInjector @WithParentInjector(A) class B2 {}
@GenerateInjector @WithParentInjector(B2) class C2 {}
@GenerateInjector @WithParentInjector(C2) class D2 {}
@GenerateInjector @WithParentInjector(A) class X {}
@GenerateInjector @WithParentInjector(C) class Y {}
// creating injectors
var injectorA = N4Injector.of(A);
//following throws DIConfigurationError, expected parent is not provided
//var injectorB = N4Injector.of(B);
//correct declarations
var injectorB = N4Injector.of(B, injectorA);
var injectorC = N4Injector.of(C, injectorB);
var injectorD = N4Injector.of(D, injectorC);
var injectorB2 = N4Injector.of(B2, injectorA);
var injectorC2 = N4Injector.of(C2, injectorB2);
var injectorD2 = N4Injector.of(D2, injectorC2);
//Any injector of {A,B,C,D,b2,C2,D2} s valid parent for injector of X, e.g. D or D2
N4Injector.of(X, injectorD);//is ok as compatible parent is provided
N4Injector.of(X, injectorD2);//is ok as compatible parent is provided
N4Injector.of(Y, injectorC);//is ok as direct parent is provided
N4Injector.of(Y, injectorD);//is ok as compatible parent is provided
N4Injector.of(Y, injectorB2);//throws DIConfigurationError, incompatible parent is provided
N4Injector.of(Y, injectorC2);//throws DIConfigurationError, incompatible parent is provided
N4Injector.of(Y, injectorD2);//throws DIConfigurationError, incompatible parent is provided
11.2.2. Binders and Bindings
Binder allows an N4JS developer to (explicitly) define a set of Bindings that will be used by an Injector configured with a given Binder.
There are two ways for Binder to define Bindings: @Bind
(N4JS DI @Bind) annotations and a method annotated with @Provides
.
Binder is declared by annotating a class with the @Binder
annotation.
A Binding is part of a configuration that defines which instance of what type should be injected into an injection point (Injection Points) with an expected type.
Provider Method is essentially a factory method that is used to create an instance of a type. N4JS allows a developer to declare those methods (see N4JS DI @Provides) which gives them a hook in instance creation process. Those methods will be used when creating instances by the Injector configured with the corresponding Binder. A provider method is a special kind of binding () in which the return type of the method is the . The type is unknown at compile time (although it may be inferred by examining the return statements of the provide method).
Definition: Binding
A binding is a pair . It defines that for a dependency with a given key which usually is the expected type at the injection point. An instance of type is injected.
A binding is called explicit if it is declared in the code, i.e. via @Bind
annotation or @Provides
annotation).
A binding is called implicit if it is not declared. An implicit binding can only be used if the is a class and derived from the type at the injection point, i.e. the type of the field or parameter to be injected. In that case, the equals the .
A provider method (in the binder) defines a binding
(in which is an existential type with ).
For simplification, we define:
and
Req. IDE-140: Bindings (ver. 1)
For a given binding , the following constraints must hold: [57]
-
must be either a class or an interface.
-
must either be a class or a provider method.
-
If is implicit, then must be a class. If references a type , then – even if is a use-site structural type.
-
and can be nominal, structural or field-structural types, either definition-site or use-site. The injector and binder needs to take the different structural reference into account at runtime!
-
must hold
-
If during injection phase no binding for a given key is found, an
DIUnsatisfiedBindingError
is thrown.
Req. IDE-141: Transitive Bindings (ver. 1)
If an injector contains two given bindings and , an effective binding is derived (replacing ).
N4JS DI mechanisms don’t allow for injection of primitives or built-in types. Only user-defined N4Types can be used. In cases where a user needs to inject a primitive or a built-in type, the developer must wrap it into its own class [58]. This is to say that none of the following metatypes can be bound: primitive types, enumerations, functions, object types, union- or intersection types. It is possible to (implicitly) bind to built-in classes.
While direct binding overriding or rebinding is not allowed, Injector can be configured in a way where one type can be separately bound to different types with implicit binding, explicit binding and in bindings of the child injectors. Binding precedence is a mechanism of Injector selecting a binding use for a type. It operates in the following order:
-
Try to use explicit binding, if this is not available:
-
Try to delegate to parent injectors (order of lookup is not guaranteed, first found is selected). If this is not available then:
-
Try to use use implicit binding, which is simply to attempt to create the instance.
If no binding for a requested type is available an error will be thrown.
11.2.3. Injection Points
By injection point we mean a place in the source code which, at runtime, will be expected to hold a reference to a particular type instance.
11.2.3.1. Field Injection
In its simplest form, this is a class field annotated with @Inject
annotation.
At runtime, an instance of the containing class will be expected to hold reference to an instance of the field declared type.
Usually that case
is called Field Injection.
Req. IDE-142: Field Injection (ver. 1)
The injector will inject the following fields:
-
All directly contained fields annotated with
@Inject
. -
All inherited fields annotated with
@Inject
. -
The injected fields will be created by the injector and their fields will be injected as well.
Simple Field Injection demonstrates simple field injection using default bindings.
Note that all inherited fields (i.e. A.xInA
) are injected and also fields in injected fields (i.e. x.y
)
class X {
@Inject y: Y;
}
class Y {}
class A {
@Inject xInA: X;
}
class B extends A {
@Inject xInB: X;
}
@GenerateInjector
export public class DIC {
@Inject a: B;
}
var dic = N4Injector.of(DIC).create(DIC);
console.log(dic); // --> DIC
console.log(dic.a); // --> B
console.log(dic.a.xInA); // --> X
console.log(dic.a.xInA.y); // --> Y
console.log(dic.a.xInB); // --> X
console.log(dic.a.xInB.y); // --> Y
11.2.3.2. Constructor Injection
Parameters of the constructor can also be injected, in which case this is usually referred to as Constructor Inejction. This is similar to Method Injection and while constructor injection is supported in N4JS, method injection is not (see remarks below).
When a constructor is annotated with @Inject
annotation, all user-defined, non-generic types given as the parameters will be injected into the instance’s constructor created by the dependency injection framework.
Currently, optional constructor parameters are always initialized and created by the framework, therefore, they are ensured to be available at the constructor invocation time.
Unlike optional parameters, variadic parameters cannot be injected into a type’s constructor.
In case of annotating a constructor with @Inject
that has variadic parameters, a validation error will be reported.
When a class’s constructor is annotated with @Inject
annotation, it is highly recommended to annotate all explicitly-defined constructors at the subclass level.
If this is not done, the injection chain can break and runtime errors might occur due to undefined constructor parameters.
In the case of a possible broken injection chain due to missing @Inject
annotations for any subclasses, a validation warning will
be reported.
Req. IDE-143: Constructor Injection (ver. 1)
If a class has a constructor marked as injection point, the following applies:
-
If is subclassed by , and if has no explicit constructor, then inherits the constructor from and it will be an injection point handled by the injector during injection phase.
-
If provides its own injector, is no longer recognized by the injector during the injection phase. There will be a warning generated in to mark it as injection point as well in order to prevent inconsistent injection behavior. Still, must be called in similarly to other overridden constructors.
11.2.3.3. Method Injection
Other kinds of injector points are method parameters where (usually) all method parameters are injected when the method is called. In a way, constructor injection is a special case of the method itself.
11.2.3.3.1. Provider
Provider is essentially a factory for a given type.
By injecting an N4Provider
into any injection point, one can acquire new instances of a given type provided by the injected provider.
The providers prove useful when one has to solve re-injection issues since the depended type can be wired and injected via the provider rather than the dependency itself and can therefore obtain
new instances from it if required.
Provider can be also used as a means of delaying the instantiation time of a given type.
N4Provider
is a public generic built-in interface that is used to support the re-injection.
The generic type represents the dependent type that has to be obtained.
The N4Provider
interface has one single public method: public T get()
which should be invoked from the client code when a new instance of the dependent type is required.
Unlike any other unbound interfaces, the N4Provider
can be injected without any explicit binding.
The following snippet demonstrates the usage of N4Provider
:
class SomeService { }
@Singleton
class SomeSingletonService { }
class SomeClass {
@Inject serviceProvider: N4Provider<SomeService>;
@Inject singletonServiceProvider: N4Provider<SomeSingletonService>;
void foo() {
console.log(serviceProvider.get() ===
serviceProvider.get()); //false
console.log(singletonServiceProvider.get() ===
singletonServiceProvider.get()); //true
}
}
It is important to note that the N4Provider
interface can be extended by any user-defined interfaces and/or can be implemented by any user-defined classes.
For those user-defined providers, consider all binding-related rules; the extended interface, for example, must be explicitly bound via a binder to be injected.
The binding can be omitted only for the built-in N4Provider
s.
11.2.4. N4JS DI Life Cycle and Scopes
DI Life Cycle defines when a new instance is created by the injector as its destruction is handled by JavaScript.
The creation depends on the scope of the type.
Aside from the scopes, note that it is also possible to implement custom scopes and life cycle management via N4JSProvider
and Binder@Provides
methods.
11.2.4.1. Injection Cylces
Definition: Injection Cycle
We define an injection graph as a directed graph as follows: (the vertices) is the set types of which instances are created during the injection phase and which use . (the edges) is a set of directed and labeled edges , where label indicates the injection point:
-
, if is the actualy type of an an injected field of an instance of type
-
, if is the type of a parameter used in a constructor injection of type
One cycle in this graph is an injection cycle.
When injecting instances into an object, cycles have to be detected and handled independently from the scope. If this is not done, the following examples would result in an infinite loop causing the entire script to freeze until the engine reports an error:
|
Figure 10. Field Cycle
|
|
Figure 10. Ctor Field Cycle
|
|
Figure 10. Ctor Cycle
|
The injector needs to detect these cycles and resolve them.
Req. IDE-144: Resolution of Injection Cycles (ver. 1)
A cycle , with being an injection graph, is resolved as follows:
-
If contains no edge with , the cycle is resolved using the algorithm described below.
-
If contains at least one edge with , a runtime exception is thrown.
Cycles stemming from field injection are resolved by halting the creation of new instances of types which have been already created by a containing instance. The previously-created instance is then reused. This makes injecting the instance of a (transitive) container less complicated and without the need to pass the container instance down the entire chain. The following pseudo code describes the algorithm to create new instances which are injected into a newly created object:
function injectDependencies(object) {
doInjectionWithCylceAwareness(object, {(typeof object -> object)})
}
function doInjectionWithCylceAwareness(object, createdInstancesPerType) {
forall v $\in$ injectedVars of object {
var type = retrieveBoundType(v)
var instance = createdInstancesPerType.get(type)
if (not exists instance) {
instance = createInstance(type, createdInstancesPerType)
doInjectionWithCylceAwareness(instance,
createdInstancesPerType $\cap$ {(type->instance)})
}
v.value = instance;
}
}
The actual instance is created in line 10 via createInstance
.
This function then takes scopes into account.
The createdInstancesPerType
map is passed to that function in order to enable cycle detection for constructor injection.
The following scopes are supported by the N4JS DI, other scopes, cf. Jersey custom scopes and Guice custom scopes, may be added in the future.
This algorithm is not working for constructor injection because it is possible to already access all fields of the arguments passed to the constructor. In the algorithm, however, the instances may not be completely initialized.
11.2.4.2. Default Scope
The default scope always creates a new instance.
11.2.4.3. Singleton Scope
The singleton scope (per injector) creates one instance (of the type with @Singleton
scope) per injector, which is then shared between clients.
The injector will preserve a single instance of the type of S
and will provide it to all injection points where type of S
is used.
Assuming nested injectors without any declared binding where the second parameter is S
, the same preserved singleton instance will be available for all nested injectors at all injection points as well.
The singleton preservation behavior changes when explicit bindings are declared for type S
on the nested injector level.
Let’s assume that the type S
exists and the type is annotated with @Singleton
.
Furthermore, there is a declared binding where the binding’s second argument is S
.
In that case, unlike in other dependency injection frameworks, nested injectors may preserve a singleton for itself and all descendant injectors with @Bind
annotation.
In this case, the preserved singleton at the child injector level will be a different instance than the one at the parent injectors.
The tables below depict the expected runtime behavior of singletons used at different injector levels.
Assume the following are injectors: C
, D
, E
, F
and G
. Injector C
is the top most injector and its nesting injector D
, hence injector C
is the parent of the injector D
.
Injector D
is nesting E
and so on.
The most nested injector is G
. Let’s assume J
is an interface, class U
implements interface J
and class V
extends class U
.
Finally assume both U
and V
are annotated with @Singleton
at definition-site.
DI No Bindings depicts the singleton preservation for nested injectors without any bindings.
All injectors use the same instance from a type.
Type J
is not available at all since it is not bound to any concrete implementation:
Binding |
|||||
---|---|---|---|---|---|
Injector nesting () |
C |
D |
E |
F |
G |
J |
|||||
U |
|||||
V |
DI Transitive Bindings is configured by explicit bindings. At the root injector level, type J
is bound to type U
.
Since the second argument of the binding is declared as a singleton at the definition-site,
this explicit binding implicitly ensures that the injector and all of its descendants preserve a singleton of the bound type U
.
At injector level C
, D
and E
, the same instance is used for type J
which is type U
at runtime.
At injector level E
there is an additional binding from type U
to type V
that overrules the binding declared at the root injector level.
With this binding, each places where J
is declared, type U
is used at runtime.
Furthermore, since V
is declared as a singleton, both injector F
and G
are using a shared singleton instance of type V
.
Finally, for type V
, injector C
, D
and E
should use a separate instance of V
other than injector level F
and G
because V
is preserved at injector level F
with the U
V
binding.
Binding | J → U | U → V | |||
---|---|---|---|---|---|
Injector nesting (>) |
C |
D |
E |
F |
G |
J |
|||||
U |
|||||
V |
DI Re - Binding depicts the singleton behaviour but unlike the above
table, the bindings are declared for the interface J
.
Binding | J → U | J → V | |||
---|---|---|---|---|---|
Injector nesting () |
C |
D |
E |
F |
G |
J |
|||||
U |
|||||
V |
DI Child Binding describes the singleton behavior when both bindings are configured at child injector levels but not the root injector level.
Binding | U V | J U | |||
---|---|---|---|---|---|
Injector nesting () |
C |
D |
E |
F |
G |
J |
|||||
U |
|||||
V |
11.2.4.4. Per Injection Chain Singleton
The per injection chain singleton is ’between’ the default and singleton scope. It can be used in order to explicitly describe the situation which happens when a simple cycle is resolved automatically. It has more effects that lead to a more deterministic behavior.
Assume a provider declared as
var pb: Provider<B>;
to be available:
@PerInjectionSingleton
class A { }
class B { @Inject a: A; @Inject a1: A;}
b1=pb.get();
b2=pb.get();
b1.a != b2.a
b1.a == b1.a1
b2.a == b2.a1
@Singleton
class A { }
class B { @Inject a: A; @Inject a1: A;}
b1=pb.get();
b2=pb.get();
b1.a == b2.a
b1.a == b1.a1
b2.a == b2.a1
// no annotation
class A { }
class B { @Inject a A; @Inject a1: A;}
b1=pb.get();
b2=pb.get();
b1.a != b2.a
b1.a != b1.a1
b2.a != b2.a1
11.2.5. Validation of callsites targeting N4Injector methods
Terminology for this section:
-
a value is injectable if it
-
either conforms to a user-defined class or interface (a non-parameterized one, that is),
-
or conforms to Provider-of-T where T is injectable itself.
-
-
a classifier declaring injected members is said to require injection
To better understand the validations in effect for callsites targeting
N4Injector.of(ctorOfDIC: constructor{N4Object}, parentDIC: N4Injector?, ...providedBinders: N4Object)
we can recap that at runtime:
-
The first argument denotes a DIC constructor.
-
The second (optional) argument is an injector.
-
Lastly, the purpose of
providedBinders
is as follows:-
The DIC above is marked with one or more
@UseBinder
. -
Some of those binders may require injection.
-
Some of those binders may have constructor(s) taking parameters.
-
The set of binders described above should match the providedBinders.
-
Validations in effect for N4Injector.create(type{T} ctor)
callsites:
-
type{T}
should be injectable (in particular, it may be anN4Provider
).
11.2.6. N4JS DI Annotations
Following annotations describe API used to configure N4JSDI.
11.2.6.1. N4JS DI @GenerateInjector
|
|
|
|
|
|
@GenerateInjector
marks a given class as DIComponent of the graph.
The generated injector will be responsible for creating an instance of that class and all of its dependencies.
11.2.6.2. N4JS DI @WithParentInjector
|
|
|
|
|
|
@WithParentInjector
marks given injector as depended on other injector.
The depended injector may use provided injector to create instances of objects required in its object graph.
Additional WithParentInjector constraints:
Req. IDE-145: DI WithParentInjector (ver. 1)
-
Allowed only on
N4ClassDeclarations
annotated with@GenerateInjector
. -
Its parameter can only be
N4ClassDeclarations
annotated with .
11.2.6.3. N4JS DI @UseBinder
|
|
|
|
|
|
@UseBinder
describes Binder to be used (configure) target Injector.
Req. IDE-146: DI UseInjector (ver. 1)
-
Allowed only on
N4ClassDeclarations
annotated with@GenerateInjector
. -
Its parameter can only be
N4ClassDeclarations
annotated with@Binder
.
11.2.6.4. N4JS DI @Binder
|
|
|
|
|
|
@Binder
defines a list of bind configurations.
That can be either @Bind
annotations on @Binder
itself or its factory methods annotated with @Provides
.
Req. IDE-147: DI binder (ver. 1)
-
Target
N4ClassDeclaration
must not be abstract. -
Target
N4ClassDeclaration
must not be annotated with@GenerateInjector
. -
Target class cannot have injection points.
11.2.6.5. N4JS DI @Bind
|
|
|
|
|
|
Defines binding between type and subtype that will be used by injector when configured with target N4JS DI @Binder. See also Validation of callsites targeting N4Injector methods for description of injectable types.
Req. IDE-148: DI Bind (ver. 1)
-
Allowed only on
N4ClassDeclarations
that are annotated with@Binder
(N4JS DI @Binder). -
Parameters are instances of one of the values described in Validation of callsites targeting N4Injector methods.
-
The second parameter must be a subtype of the first one.
11.2.6.6. N4JS DI @Provides
|
|
|
|
|
|
@Provides
marks factory method to be used as part DI.
This is treated as explicit binding between declared return type and actual return type.
This method is expected to be part of the @Binder
.
Can be used to implement custom scopes.
Req. IDE-149: DI Provides (ver. 1)
-
Allowed only on
N4MethodDeclarations
that are part of a classifier annotated with@Binder
. -
Annotated method declared type returns instance of one of the types described in injectable values Validation of callsites targeting N4Injector methods.
11.2.6.7. N4JS DI @Inject
|
|
|
|
|
|
@Inject
defines the injection point into which an instance object will be injected.
The specific instance depends on the injector configuration (bindings) used.
Class fields, methods and constructors can be annotated. See Injection Points for more information.
Req. IDE-150: DI Inject (ver. 1)
-
Injection point bindings need to be resolvable.
-
Binding for given type must not be duplicated.
-
Annotated types must be instances of one of the types described in Validation of callsites targeting N4Injector methods.
11.2.6.8. N4JS DI @Singleton
|
|
|
|
|
|
In the case of annotating a class S
with @Singleton
on the definition-site, the singleton scope will be used as described in Singleton Scope.
11.3. Test Support
N4JS provides some annotations for testing. Most of these annotations are similar to annotations found in JUnit 4. For details see our Mangelhaft test framework (stdlib specification) and the N4JS-IDE specification.
In order to enable tests for private methods, test projects may define which project they are testing.
Req. IDE-151: Test API methods and types (ver. 1)
In some cases, types or methods are only provided for testing purposes.
In order to improve usability, e.g. content assist, these types and methods can be annotated with @TestAPI
.
There are no constraints defined for that annotation at the moment.
11.4. Polyfill Definitions
In plain JavaScript, so called polyfill (or sometimes called shim) libraries are provided in order to modify existing classes which are only prototypes in plain JavaScript.
In N4JS, this can be defined for declarations via the annotation @Polyfill
or @StaticPolyfill
.
One of these annotations can be added to class declarations which do not look that much different from normal classes.
In the case of polyfill classes, the extended class is modified (or filled) instead of being subclassed. It is therefore valid to polyfill a class even if it is declared @Final
.
We distinguish two flavours of polyfill classes: runtime and static.
-
Runtime polyfilling covers type enrichment for runtime libraries. For type modifications the annotation
@Polyfill
is used. -
Static polyfilling covers code modifications for adapting generated code. The annotation
@StaticPolyfill
denotes a polyfill in ordinary code, which usually provides executable implementations.
Definition: Polyfill Class
A polyfill class (or simply polyfill) is a class modifying an existing one. The polyfill is not a new class (or type) on its own. Instead, new members defined in the polyfill are added to the modified class and existing members can be modified similarly to overriding. We call the modified class the filled class and the modification filling.
We add a new pseudo property to classes in order to distinguish between normal (sub-) classes and polyfill classes.
Req. IDE-152: Polyfill Class (ver. 1)
For a polyfill class annotated with @Polyfill
or @StaticPolyfill
, that is , all the following constraints must hold:
-
must extend a class , is called the filled class:
-
’s name equals the name of the filled class and is contained in a module with same qualified name (specifier or global):
-
Both the polyfill and filled class must be top-level declarations (i.e., no class expression):
-
must not implement any interfaces:
-
must have the same access modifier (access, abstract, final) as the filled class:
-
If declares a constructor, it must be override compatible with the constructor of the filled class:
-
must define the same type variables as the filled class and the arguments must be in the same order as the parameters (with no further modifications):
-
All constraints related to member redefinition (cf. Redefinition of Members) have to hold. In the case of polyfills, this is true for constructors (cf. [Req-IDE-72]) and private members.
11.4.1. Runtime Polyfill Definitions
(Runtime) Libraries often do not provide completely new types but modify existing types.
The ECMA-402 Internationalization Standard [ECMA12a], for example, changes methods of the built-in class Date
to be timezone aware.
Other scenarios include new functionality provided by browsers which are not part of an official standard yet.
Even ECMAScript 6 [ECMA15a] extends the predecessor [ECMA11a] in terms of new methods (or new method parameters) added to existing types (it also adds completely new classes and features, of course).
Runtime polyfills are only applicable to runtime libraries or environments and thus are limited to n4jsd files.
Req. IDE-153: Runtime Polyfill Class (ver. 1)
For a runtime-polyfill class annotated with @Polyfill
, that is , all the following constraints must hold in addition to [Req-IDE-152]:
-
Both the polyfill and filled class are provided by the runtime (annotated with
@ProvidedByRuntime
): [59]
Req. IDE-154: Applying Polyfills (ver. 1)
A polyfill is automatically applied if a runtime library or environment required by the current project provides it. In this case, the following constraints must hold:
-
No member must be filled by more than one polyfill.
11.4.2. Static Polyfill Definitions
Static polyfilling is a compile time feature to enrich the definition and usually also the implementation of generated code in N4JS. It is related to runtime polyfilling described in Runtime Polyfill Definitions in a sense that both fillings enrich the types they address. Despite this, static polyfilling and runtime polyfilling differ in the way they are handled.
Static polyfills usually provide executable implementations and are thus usually found in n4js files. However, they are allowed in n4jsd files, as well, for example to enrich generated code in an API project.
The motivation for static polyfills is to support automatic code generation.
In many cases, automatically generated code is missing some information to make it sufficiently usable in the desired environment.
Manual enhancements usually need to be applied.
If we think of a toolchain, the question may arise how to preserve the manual work when a
regeneration is triggered. Static polyfilling allows the separation of generated code and manual adjustments in separate files.
The transpiler merges the two files into a single transpiled file.
To enable this behaviour, the statically fillable types must be contained in a module annotated with @StaticPolyfillAware
.
The filling types must also be annotated with @StaticPolyfill
and be contained in a different module with same specifier but annotated with @StaticPolyfillModule
.
Static polyfilling is restricted to a project, thus the module to be filled as well as the filling module must be contained in the same project.
We add a new pseudo property to classes in order to distinguish between normal (sub-) classes and static polyfill classes. We add two new pseudo properties to modules in order to modify the transpilation process. The mutually-exclusive properties and signal the way these files are processed.
In order to support efficient transpilation, the following constraint must hold in addition to constraints:
Req. IDE-155: Static Polyfill Layout (ver. 1)
For a static polyfill class annotated with @StaticPolyfill
, that is , all the following constraints must hold in addition to [Req-IDE-152]:
-
’s name equals the name of the filled class and is contained in a module with the same qualified name:
-
Both the static polyfill and the filled class are part of the same project:
-
The filled class must be contained in a module annotated with
@StaticPolyfillAware
: -
The static polyfill and the filled type must both be declared in an n4js file or both in an n4jsd file.
-
The filling class must be contained in a module annotated with
@StaticPolyfillModule
: -
For a statically-filled class there is at most one static polyfill:
Req. IDE-156: Restrictions on static polyfilling (ver. 1)
For a static polyfilling module the following must hold:
-
All top-level elements are static polyfills:
-
It exists exactly one filled module annotated with in the same project.
-
It is an error if two static polyfill modules for the same filled module exist in the same project:
Static Polyfill, Genmod shows an example of generated code. Static Polyfill, Polyfillmod demonstrates the static polyfill.
Note that the containing project has two source folders configured:
Project/src/n4js
and Project/src/n4jsgen
.
@@StaticPolyfillAware
export public class A {
constructor() {...}
m1(): void{...}
}
export public class B {
constructor() {...}
m2(): void{...}
}
@@StaticPolyfillModule
@StaticPolyfill
export public class B extends B {
@Override
constructor(){ ... } // replaces generated ctor of B
@Override
m1(): void {...} // adds overridden method m1 to B
@Override
m2(): void {...} // replaces method m2 in B
m3(): void {...} // adds new method m3 to B
}
11.4.3. Transpiling static polyfilled classes
Transpiling static polyfilled classes encounters the special case that two different n4js
source files with the same qualified name are part of the project.
Since the current transpiler is file-based, both files would be transpiled to the same output destination and would therefore overwrite each other.
The following pre-transpilation steps handle this situation:
-
Current file to transpile is
-
If , then
-
search for a second file with same qualified name:
-
If , then
-
merge into current file
-
conventionally transpile
-
-
else conventionally transpile
-
-
else, if ,
-
then do nothing. (Transpilation will be triggered for filled type separately.)
-
-
else, conventionally transpile