Component
(require component) | package: component-lib |
1 Introduction
This library helps you manage the life-cycle of stateful components in your application. It ensures that they are started, linked together and, finally, stopped in the correct order.
By writing programs in this style, you trade some flexibility for clarity around how and when the individual parts of your application are initialized.
1.1 Guide
Let’s assume that you’re writing a web application that emails users when they sign up. Your components might be:
the database,
the mailer,
the user manager, which depends on the database, and
the http frontend, which depends on the mailer and the user manager.
Given those components, your system might look like this:
(define-system prod [db make-database] [mailer make-mailer] [users (db) (lambda (db) (make-user-manager db))] [http (mailer users) (lambda (m um) (make-http m um))]) (system-start prod-system) (system-stop prod-system)
The system is made up of a list of component declarations where each one is made up of the unique id of a component in the system, an optional list of dependencies (other component ids) and a function that can be used to construct that component from its dependencies. There are no constraints on the ids of the components in the system and you can have multiple components of the same type (for example, read-only and read-write databases).
The define-system form creates a value that represents the system and its internal dependency graph but does not start it.
The call to system-start starts the db and the mailer first (one or the other may be started first since neither has any dependencies), then the user-manager and finally the http server.
Finally, the call to system-stop stops all the components in the system in the reverse order that they were started in.
1.2 Limitations
Components that have no dependencies and no dependents are never started.
2 Reference
2.1 Components
Components are plain structs that implement the gen:component interface. All that’s required of a component is that it needs to know how to start and then stop itself.
Here’s a component that doesn’t do anything except flip a flag when it gets started and stopped:
> (struct mailer (started?) #:transparent #:methods gen:component [(define (component-start _) (mailer #t)) (define (component-stop _) (mailer #f))])
> (define m (mailer #f)) > (mailer-started? m) #f
> (mailer-started? (component-start m)) #t
And here’s what a component that encapsulates a database connection pool might look like:
> (struct db (connector custodian pool) #:transparent #:methods gen:component [(define (component-start the-db) (define custodian (make-custodian)) (struct-copy db the-db [custodian custodian] [pool (parameterize ([current-custodian custodian]) (connection-pool (db-connector the-db)))])) (define (component-stop the-db) (custodian-shutdown-all (db-custodian the-db)) (struct-copy db the-db [custodian #f] [pool #f]))])
> (define (make-db connector) (db connector #f #f))
> (component-start (make-db (lambda () (sqlite3-connect #:database 'temporary)))) (db #<procedure> #<custodian> (object:connection-pool% ...))
interface
procedure
(component? v) → boolean?
v : any/c
procedure
(component-start c) → component?
c : component?
procedure
(component-stop c) → component?
c : component?
2.1.1 Wrapper Components
Wrapper components transparently wrap values that cannot implement the component interface themselves.
> (struct a (n)) > (struct b (a))
> (struct c (a) #:methods gen:component [(define (component-start self) (c (a 42)))] #:methods gen:wrapper-component [(define (component-unwrap self) (c-a self))])
> (define-system wrapped [b (a) b] [a (lambda () (c #f))]) > (system-start wrapped-system) > (a? (system-ref wrapped-system 'a)) #t
> (system-stop wrapped-system)
interface
Added in version 1.3 of package component-lib.
procedure
(wrapper-component? v) → boolean?
v : any/c
procedure
(component-unwrap w) → any/c
w : wrapper-component?
2.2 Systems
Systems group components together according to a declarative specification.
When a system is started, its components are started in dependency order (if a depends on b which depends on c then c is started first, then b then a) and injected into their dependents’ factory functions (c is passed to b which is finally passed to a).
When a system is stopped, its components are stopped in the reverse order that they were started in.
procedure
(make-system spec) → system?
spec :
(listof (or/c (list/c symbol? any/c) (list/c symbol? (listof symbol?) any/c)))
syntax
(define-system id component ...+)
component = [component-id factory-expr] | [component-id (dependency-id ...) factory-expr]
factory-expr : (-> any/c ... any/c)
(define-system prod [db make-db] [app (db) make-app])
defines a system called prod-system.
procedure
(system-start s) → void?
s : system?
procedure
(system-stop s) → void?
s : system?
procedure
(system-ref id) → any/c
id : symbol? (system-ref s id) → any/c s : system? id : symbol?
The first variant attempts to look up id from the current-system, failing if one isn’t installed.
procedure
(system-replace s id factory) → system?
s : system? id : symbol? factory : any/c
procedure
(system->dot s) → string?
s : system?
procedure
(system->png s output-path) → boolean?
s : system? output-path : path-string?
2.3 The Escape Hatch
Manually passing components around can be painful in highly-dynamic applications so the library provides an escape hatch for those use-cases. When a system is started, current-system is parameterized to point to the system itself. That way, components’ start and stop functions as well as any threads started by components are able to directly reference the system they are a part of.
parameter
(current-system) → (or/c false/c system?)
(current-system s) → void? s : (or/c false/c system?)
2.4 Testing
(require component/testing) | package: component-lib |
When integration testing components, you often need to put together a subset of components, start them up and reference them from within your tests. The system-test-suite form provides a convenient way to do this.
syntax
(system-test-suite id (component ...+) maybe-before maybe-after body-expr ...+)
component =
(component-id factory-expr) (component-id (dependency-id ...) factory-expr) maybe-before =
| #:before before-expr maybe-after =
| #:after after-expr
> (struct mailer () #:methods gen:component [])
> (struct app (mailer) #:methods gen:component [])
> (run-tests (system-test-suite app ([a (m) app] [m mailer]) (test-case "instantiated" (check-true (system? app-system)) (check-true (app? a)) (check-true (mailer? (app-mailer a)))))) 1 success(es) 0 failure(s) 0 error(s) 1 test(s) run
0
3 Acknowledgments
This library draws inspiration from Stuart Sierra’s "component" library for Clojure.