Configurable
This library implements a simple system for software configuration. The idea is that the system implementor can create configurable features which have multiple implementations that users can select from. Users can then create a config that selects an implementation for each configurable feature. The system can install such a config to configure the features.
This library supports that workflow with the following design.
an interface of values related to the feature
one or more implementations, identified by module paths.
With the configurable feature set definition in hand, the system implementor can require it to access each featureās interface through parameters holding their currently configured values.
A config defines a mapping from each featureās interface to concrete values by selecting an implementation for each feature. The system sets the current configuration values by installing a config with install-configuration!. Configs are written in the #lang configurable/config DSL.
1 A tour by example
This example illustrates implementing a tool that searches text by lines (think grep). The tool should allow users to select a search algorithm from three choices: literal matching, regexp, and fuzzy search (at least initially - we will see how the this library makes it easy to add more down the road).
Our toolās implementation might initially look like this.
;; tool.rkt #lang racket ;; swap out with regexp.rkt or fuzzy.rkt (require "literal.rkt") (module+ main (require racket/cmdline) (define-values {query files-to-search} (command-line #:args [query . files-to-search] (values query files-to-search))) (search! query files-to-search))
With the different search styles implemented in their own files.
;; literal.rkt #lang racket (provide (rename-out [search/literal! search!])) (define (search/literal! str files) ;; todo... (displayln 'searching-literally))
;; regexp.rkt #lang racket (provide (rename-out [search/regexp! search!])) (define (search/regexp! rx files) ;; todo... (displayln 'searching-for-regexp))
;; fuzzy.rkt #lang racket (provide (rename-out [search/fuzzy! search!])) (define (search/fuzzy! pat files) ;; todo... (displayln 'searching-fuzzily))
To let users select a search style via a config file instead of modifying the tool, weāll write a configurable feature set definition describing our configurable feature (the search style) and our known implementations. Then weāll write a config that selects a style, which users can edit instead of editing the tool itself.
The feature set definition looks like this.
;; configurables.rkt #lang configurable/definition (define-configurable search-style #:provides [search!] (define-implementation literal #:module "literal.rkt") (define-implementation regexp #:module "regexp.rkt") (define-implementation fuzzy #:module "fuzzy.rkt"))
And a config file looks like this.
;; search-config.rkt #lang configurable/config "configurables.rkt" ;; `fuzzy` here is bound by the definition in configurables.rkt (configure-all! [search-style fuzzy])
Finally we refactor our tool to look like this. Each change is annotated with a comment.
;; tool.rkt #lang racket (require "configurables.rkt") ;; require the feature set definition (module+ main (require racket/cmdline) (define-values {query files-to-search} (command-line ;; obtain the config path... #:once-each [("--config" "-c") path "search style configuration to use" ;; ... and install it (install-configuration! path)] #:args [query . files-to-search] (values query files-to-search))) ;; access the configured search function with the parameter `configured:search!` ;; which is created by `define-configurable!` ((configured:search!) query files-to-search))
Under the hood, whatās happening is that install-configuration! sets the value of the parameter configured:search!.
Of course, this example is a bit contrived because a single command-line switch would suffice to configure the search style. However, that approach quickly grows unwieldy when there are several features to configure, and especially so if some implementations themselves may be parameterized.
This library offers a natural solution for the second challenge of parameterized features as well, with implementation parameters. Implementation parameters are essentially arguments that can be specified in a config file to configure an implementation.
Letās see how that works by adding a new feature to our search tool. Weāll support abbreviations in literal search queries, so that doing a literal search for @myemail instead searches for joe-schmoe9000@gmail.com. A table of abbreviation definitions in the userās config file will define the set of these to use.
First, letās update literal.rkt to support these abbrevs.
;; literal.rkt #lang racket (provide (rename-out [search/literal! search!]) current-abbrevs) (define current-abbrevs (make-parameter (hash))) (define (search/literal! str files) ;; todo... (displayln 'searching-literally/with-abbrevs) (displayln (current-abbrevs)))
Next, letās update the configurable feature set definition.
;; configurables.rkt #lang configurable/definition (define-configurable search-style #:provides [search!] (define-implementation literal #:module "literal.rkt" #:parameters [current-abbrevs]) (define-implementation regexp #:module "regexp.rkt") (define-implementation fuzzy #:module "fuzzy.rkt"))
And finally we can use it in the config.
;; search-config.rkt #lang configurable/config "configurables.rkt" (configure-all! [search-style literal (hash "@myemail" "joe-schmoe9000@gmail.com")])
The config would not need to change at all for other search styles, on the other hand. For instance, the same fuzzy-searching config we had before is still valid now.
2 Configurable feature set definition DSL
#lang configurable/definition | package: configurable |
configurable feature set definitions are written in this language, and consist of a sequence of define-configurable forms at the top level.
The resulting module provides the names of all the defined configurable features, the features configured: parameters, and the operations on configs described in Config operations.
syntax
(define-configurable feature-id #:provides [id ...] implementation-definition ...)
Each implementation-definition must be an define-implementation form.
This form creates and provides a parameter for each id named configured:id which will hold the currently configured implementationās version of id.
syntax
(define-implementation implementation-id #:module relative-module-path maybe-parameters)
maybe-parameters =
| #:parameters [parameter-id ...]
Each parameter-id specified defines an implementation parameter.
3 Config DSL
#lang configurable/config | package: configurable |
configs are written in this language, which is parameterized by the configurable feature set definition whose relative path is provided after the #lang (see the example configs above). Configs consist of a sequence of configure! or configure-all! forms at the top level.
syntax
(configure! feature-id implementation-id parameter-value ...)
syntax
(configure-all! [feature-id implementation-id parameter-value ...] ...)
4 Config operations
(require configurable/definition) | package: configurable |
These operations are also provided by every configurable feature set definition, which is the more typical way to obtain them.
procedure
(install-configuration! path) ā any
path : path-string?
procedure
(call-with-configuration path thunk) ā any
path : path-string? thunk : (-> any)
procedure
(current-configuration-path) ā (or/c path-string? #f)