chameleon

2022-02-20

Configuration management facilities for Common Lisp with multiple profile support.

Upstream URL

github.com/sheepduke/chameleon

Author

YUE Daian

License

MIT
README

1Chameleon

1.1Introduction

Chameleon is a configuration management library shipped with profile support. It can help you:

  1. Define configuration items with default value.
  2. Define profiles reflecting different scenarios, e.g. development, testing, production etc.
  3. Switch between several profiles just like a chameleon switching its colors!
  4. Access configuration item via functions (instead of bare-bone keywords or strings) to avoid invalid names.
  5. Extend its behavior by providing :before, :around or :after methods.

Compatibility Note

  • Since version 2.0, Chameleon no longer defines active-profile or profiles. Use *profile* as the "current" profile and *config* holds the "current" configuration instance. Also, there will be no default NIL 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. Unlike defvar, it is evaluated during every macro expansion.
  • documentation is a string, not evaluated.

It will generate:

  • A variable *profile* indicating current profile name. Defaults to NIL.
  • A class config containing configuration items as slots.
  • A variable *config* indicating current configuration instance. Defaults to NIL.
  • A generic function switch-profile that is used to switch the profile.
  • 3 zero-arity access functions data-dir, log-leve and server-port and their setf 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.

Dependencies (2)

  • alexandria
  • fiveam

Dependents (0)

    • GitHub
    • Quicklisp