bushel

Bushel is a Dependency Injection (DI) framework, written especially for the Ceylon programming language.

Bushel features the things you normally expect from modern Java DI frameworks, like

  • annotation-driven autowiring of components, and
  • qualifiers (Java CDI style)

The goal is to provide familiar and proven Java DI concepts in a more ceylonic way, primarily utilizing the expressive type system of Ceylon.

intro

Consider the Bushel scanAnnotated() function that can scan one or several packages for components to be managed by a container.

Container container = scanAnnotated<ComponentAnnotation> {
    `package se.westshed.pump`
};

Then, consider the following components residing in the package, handed to the container:

component
Heater heater() => ElectricHeater();

component
class Thermosiphon(Heater heater) satisfies Pump {
    shared actual void pump() {
        if (heater.hot) {
            print("=> => pumping => =>");
        }
    }
}

Doing

Thermosiphon t = container.get<Thermosiphon>();

will get you … a thermosiphon.

Normally, you would ask the container for a pump like this:

Pump pump = container.get<Pump>();
assert(is Thermosiphon pump);

If you suspect that there might be no Pump implementation in the container, then you can write:

Pump? p = container.get<Pump?>();

This also opens up for components having optional dependencies:

class Widget(Pump? pump) {}

qualifiers

Consider that we have two different implementations of the same Heater interface in the container.

component
Heater heater1() => ElectricHeater();

component
Heater heater2() => ManualHeater();

component
class Thermosiphon(Heater heater) satisfies Pump {
    shared actual void pump() {
        if (heater.hot) {
            print("=> => pumping => =>");
        }
    }
}

In this case, it is impossible for the Bushel container to know, which Heater implementation to use when wiring the heater dependency for the Thermosiphon component. Simply put, we have to provide a hint to the container.

Assume that we want to use the ElectricHeater.

The most type safe way of doing this, is using a Qualifier annotation, provided by the Bushel framework.

"The electric heater qualifier"
shared annotation ElectricHeaterQualifier electricHeater() 
     => ElectricHeaterQualifier();

"The qualifier class for electricHeater."
shared final sealed
annotation class ElectricHeaterQualifier()
        satisfies Qualifier<ElectricHeaterQualifier,Heater> {

    equals(Object that) => that is ElectricHeaterQualifier;
    hash => 0;
}

You can now annotate the ElectricHeater component as well as the heater parameter to be wired, like this to let the Bushel container know about the association:

component
electricHeater
shared Heater heater1() => ElectricHeater();

component
Heater heater2() => ManualHeater();

component
class Thermosiphon(shared electricHeater Heater heater) satisfies Pump {
//...
}

This now holds true:

Thermosiphon t = container.get<Thermosiphon>();
assert(is ElectricHeater heater = t.heater);

The Ceylon type system will ensure that you never accidentally annotate another type but a Heater implementation, because that will give you a compile error.

The Ceylon type system will even let you do this, inferring the right type on the fly.

Heater heater = container.getQualified(electricHeater());
By: Jonas Grönberg
Packages
se.westshed.bushel

The Bushel API

Dependencies
ceylon.collection1.2.0
ceylon.logging1.2.0
ceylon.time1.2.0
java.base (jvm)8

The Bushel API

Aliases
Artifactshared Artifact=> Module|Package

An artifact that can be deployed into a Container.

ComponentDeclarationshared ComponentDeclaration=> ClassWithInitializerDeclaration|FunctionDeclaration

A component declaration, eligible to be managed by a Container.

Annotations
componentshared ComponentAnnotation component()

Annotation to mark a top-level class or the return type of a top-level function as a container managed component.

Sample usage

component
class Importer(Database db) {
   shared void start() {
      db.insertPerson(1, "Andy");
   }
}
component
shared Sql sql(DataSource ds) => Sql(newConnectionFromDataSource(ds));
namedshared NamedQualifier named(String name)

A qualifier that can be used to associate a component with a specified name.

ComponentAnnotationshared final ComponentAnnotation

The annotation class for component().

NamedQualifiershared final NamedQualifier

The qualifier class for named().

Functions
newBundleshared Bundle newBundle<AnnotationType>({Artifact+} artifacts, Bundle? parent = null, {ComponentFilter<Anything>*} componentFilters = ...)
given AnnotationType satisfies Annotation

Creates a bundle that can be deployed into a Container.

Sample usage

value bundle = newBundle<ComponentAnnotation> {
    artifacts = {
        `package test.se.westshed.bushel.fruit`
    };
    componentFilters = {
        newComponentFilter<Fruit> {
            (ctx) {
                return ctx.proceed();
            };
        }
    };
};
value container = newContainer(bundle);
Parameters:
  • artifacts

    The artifacts that will be scanned for ComponentDeclarations annotated with the specified AnnotationType.

  • parent = null

    The parent bundle.

  • componentFilters = {}

    The ComponentFilters to apply.

newComponentFiltershared ComponentFilter<Type> newComponentFilter<Type>(Type(ComponentFilterContext<Type>) filtering)

Creates a filter that intercepts construction of components that satisfy the specified type.

Sample usage

value fooFilter = newComponentFilter<Foo>{
   (ctx) {
      print("Pre construction of Foo instance named '``ctx.componentName``'");
      Foo foo = ctx.proceed();
      print("Post construction of foo.");
      return foo;
   };
}

value fooOrBarFilter = newComponentFilter<Foo|Bar>{
   (ctx) {
      print("Pre construction of Foo or Bar.");
      Foo|Bar fooOrBar = ctx.proceed();
      print("Post construction of Foo or Bar.");
      return fooOrBar;
   };
}

value startableFilter = newComponentFilter<Startable&Service>{
   (ctx) {
      value startableService = ctx.proceed();
      startableService.start();
      return fooOrBar;
   };
}
newContainershared Container newContainer(Bundle bundle, Container? parent = null)

Creates a container using a bundle. This function allows for more advanced stuff than scanAnnotated().

Sample usage of component filters

value container = newContainer {
   newBundle<ComponentAnnotation> {
        artifacts = {
            `package com.acme.components`
        };
        componentFilters = {
            newComponentFilter<Startable> {
                (ctx) {
                    Startable startable = ctx.proceed();
                    startable.start();
                };
            }
        };
    };
};

Sample usage of a container hierarchy

value scope1Bundle = newBundle<ComponentAnnotation> {
    `package test.se.westshed.bushel.scope1`
};
value scope1 = newContainer {
    scope1Bundle;
};
value scope2Bundle = newBundle<ComponentAnnotation> {
    {`package test.se.westshed.bushel.scope2`};
    scope1Bundle;
};
value scope2 = newContainer {
    scope2Bundle;
    scope1;
};
Parameters:
  • bundle

    The bundle to deploy into the container.

  • parent = null
See also newBundle()
runshared void run()

Run the module se.westshed.bushel.

scanAnnotatedshared Container scanAnnotated<AnnotationType>({Artifact+} artifacts)
given AnnotationType satisfies Annotation

Convenience method that scans the given packages/modules for ComponentDeclarations annotated with the specified AnnotationType and returns a thread-safe Container.

Sample usage

value container = scanAnnotated<ComponentAnnotation> { 
     `package test.my.components`,
     `package test.my.componentmocks`
};
value w = container.get<Widget>();

See newContainer() for a more advanced container setup.

Parameters:
  • artifacts

    The artifacts to deploy into the container.

Interfaces
Bundleshared Bundle

A bundle, containing Artifacts and ComponentFilters, that can be deployed into a Container. Bundles can be shared between containers and can also form a hierarchy using the parent property.

ComponentFiltershared ComponentFilter<out Type = Anything>

A filter that intercepts construction of container managed components.

ComponentFilterContextshared ComponentFilterContext<out Type>

Provides the context for a ComponentFilter.

Containershared Container

A Container provides methods for accessing components.

Sample usage

Container container = scanAnnotated<ComponentAnnotation> {
 `package com.acme.components`
}
Foo foo1 = container.get<Foo>();
Foo? foo2 = container.get<Foo?>();
Foo|Bar fooOrBar = container.get<Foo|Bar>();
Foo foo3 = container.getNamed<Foo>("myFoo");
container.iterate<Action>().each((it) => it.execute());
Qualifiershared Qualifier<out Annot,out T>

Use this interface to implement a qualifier that can be used for annotating and looking up components.