chameleon
2022-02-20
Configuration management facilities for Common Lisp with multiple profile support.
1Chameleon
1.1Introduction
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
:before
,:around
or:after
methods.
Compatibility Note
- Since version 2.0, Chameleon no longer defines
active-profile
orprofiles
. Use*profile*
as the "current" profile and*config*
holds the "current" configuration instance. Also, there will be no defaultNIL
profile. 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!
1.2Installation
;; Install Chameleon.
(ql:quickload :chameleon)
;; Test if it works on your machine.
(asdf:test-system :chameleon)
1.3Usage
The main entry points of Chameleon are 2 macros: defconfig
and defprofile
, which defines the configuration set schema and profiles.
1.3.1Defining a Configuration Set with defconfig
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 defvar
, i.e. (name [initial-value [documentation]])
:
name
is a symbol not evaluated.initial-value
is a form and always evaluated. Unlikedefvar
, it is evaluated during every macro expansion.documentation
is a string, not evaluated.
It will generate:
- A variable
*profile*
indicating current profile name. Defaults toNIL
. - A class
config
containing configuration items as slots. - A variable
*config*
indicating current configuration instance. Defaults toNIL
. - A generic function
switch-profile
that is used to switch the profile. - 3 zero-arity access functions
data-dir
,log-leve
andserver-port
and theirsetf
version.
You need to manually export these symbols in order to use them outside the current package.
*Note*
The access functions will check the value of configuration item. If the value is a function, i.e. functionp
returns 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 defprofile
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.
Given the 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 defmethod
The switch-profile
generated by defconfig
is a generic function. Each defprofile
generates a implementation method that sets *profile*
and *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.