Configuration management facilities for Common Lisp with multiple profile support.
Chameleon is a configuration management library shipped with profile support. It can help you:
- Define configuration items with default value.
- Define profiles reflecting different scenarios, e.g. development, testing, production etc.
- Switch between several profiles just like a chameleon switching its colors!
- Access configuration item via functions (instead of bare-bone keywords or strings) to avoid invalid names.
- Extend its behavior by providing
- Since version 2.0, Chameleon no longer defines
*profile*as the "current" profile and
*config*holds the "current" configuration instance. Also, there will be no default
NILprofile. You cannot access any configuration value without first defining a profile.
- Since version 1.2, Chameleon no longer exports generated symbols (including those configuration accessors) by default. This is to avoid compiler complaints when a configuration item is removed. Make it explicit!
;; Install Chameleon. (ql:quickload :chameleon) ;; Test if it works on your machine. (asdf:test-system :chameleon)
The main entry points of Chameleon are 2 macros:
defprofile, which defines the configuration set schema and profiles.
1.3.1Defining a Configuration Set with
The following example demonstrates a real-world scenario taken from my Silver Brain repository.
(defpackage config (:use #:cl) (:export #:*profile* #:switch-profile #:server-port)) (in-package config) (defconfig (data-dir) (log-level :info) (server-port 5000 "The port of running server.")
The code above defines a configuration set with 3 items. Each item briefly follows the pattern of
(name [initial-value [documentation]]):
nameis a symbol not evaluated.
initial-valueis a form and always evaluated. Unlike
defvar, it is evaluated during every macro expansion.
documentationis a string, not evaluated.
It will generate:
- A variable
*profile*indicating current profile name. Defaults to
- A class
configcontaining configuration items as slots.
- A variable
*config*indicating current configuration instance. Defaults to
- A generic function
switch-profilethat is used to switch the profile.
- 3 zero-arity access functions
You need to manually export these symbols in order to use them outside the current package.
The access functions will check the value of configuration item. If the value is a function, i.e.
T, it will be invoked and the value is returned. Otherwise, the value is directly returned. It is useful in scenarios where you want to dynamically compute the value of a configuration item.
1.3.2Defining Some Profiles with
A profile consists of values for each configuration item. If an item is missing, the default value will be used.
Profiles are isolated. Switching to a profile does not modify anything, it just sets the "current profile" to it. Also changing values defined in one profile does not affect other profiles.
defconfig code above, we may write:
;; Define a profile with default values. (defprofile :default) ;; Define a profile with name :DEV. (defprofile :dev (server-port 5001) (data-dir (truename "~/temp/silver-brain/")) (log-level (lambda () (print "Evaluated on every access") (print "Definitely useless for simply T") :debug)))) ;; Port equals to the default value, i.e. 5000. (defprofile :prod (data-dir (truename "~/.silver-brain/"))) ;; When running integration tests, the port is randomly picked. ;; ;; Macro EVAL-ONCE is no more than a let-over-lambda that caches the ;; evaluation result. The function FIND-PORT:FIND-PORT is invoked only ;; once. (defprofile :test (server-port (eval-once (find-port:find-port))))
(defpackage config-user (:use #:cl)) (in-package config-user) ;; Set profile to :DEFAULT. (config:switch-profile :default) (config:server-port) ; => 5000 (13 bits, #x1388)
1.3.3Generated Helper Functions/Macros
These helper functions and/or macros will be generated into the caller package. The following example assumes that these has been exported.
;; Temporally set profile to :DEFAULT and evaluate body. (config:with-profile :default (server-port)) ; => 5000 (13 bits, #x1388)
1.3.4Extending Behavior with
switch-profile generated by
defconfig is a generic function. Each
defprofile generates a implementation method that sets
*config*. You may implement your own method to extend its behavior.
(in-package config-user) (defmethod switch-profile :after (profile) "Reset the log level of log4cl." (log4cl:configure (log-level)))
Then, every time you call
switch-profile to change the current profile, this method is called after the profile is set, thus the log4cl get reconfigured by picking up value
log-level defined in target profile.