pierre-rouleau / defconfig

A customizer for common lisp dynamic variables

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

DEFCONFIG

Status https://travis-ci.com/szos/defconfig.svg?branch=main

Currently tested against SBCL, CCL, ABCL, ECL, and Allegro CL.

Purpose

Defconfig is a customization and validation framework for places in CL. Its intended use is with user-exposed variables and accessors, to protect users from setting variables to an invalid value an potentially encountering undefined behavior. It can be used with both dynamic variables and setf-able functions. It coexists alongside setf, and does not replace it - ie you can still use setf if you want to avoid validating the value.

If you wish to just dive in, :import the symbols #:defconfig, #:setv, #:with-atomic-setv, #:define-defconfig-db, #:get-db, and #:*setv-permissiveness* and read their docstrings.

If you encounter something that doesnt work as you think it should, please open an issue here. If you describe the exhibited behavior and the expected behavior, it will be added to the test suite and fixed (hopefully quickly).

Usage

The basic usage of this system is through the macros defconfig, setv and with-atomic-setv. The macro defconfig defines a configuration object which is used to check values against a predicate. The macro setv uses these configuration objects to validate values before calling setf. An error is signalled if A) the value is invalid, B) the coerced value is invalid (when applicable) or C) the place one is trying to set doesnt have a configuration object. For A and B, restarts are put into place to ignore the invalidity and set regardless, or continue without setting. The macro with-atomic-setv collects all places set with setv in its body and if an error is signalled resets all changed values back to their original value held before the with-atomic-setv form.

Regarding This Readme

This readme is not a complete explanation of defconfig. Please see package.lisp for a list of exported symbols and see the symbol docstrings for a better idea of what it does. Most (exported) symbols are well documented, slots have decent documentation describing their purpose/use, and errors should be grokkable from their :report functions.

Basic Tutorial

Lets look at an example where one is defining a user-exposed variable for the background color of an application. Please see tests/defconfig-tests.lisp for more examples.

Defconfig

Here we define a variable called *background-color* which holds a default value of the color white. It may only hold a color object. If we try to set it to a string, we try to parse a color object from the string. If that fails or we get something other than a string, we return the original value. We do this because the coercer is only ever called on an invalid value, and if we cannot make a color object from it we want to be certain that we return an invalid value. We then tag the configuration object with “color” and “background” such that it can be searched for by those names.

(defpackage :my-app
  (:local-nicknames (:dc :defconfig))
  (:use :cl))

(in-package :my-app)

(defstruct color r g b)

(defun parse-color-from-string (string)
  (make-color :r (parse-integer (subseq string 0 2) :radix 16)
              :g (parse-integer (subseq string 2 4) :radix 16)
              :b (parse-integer (subseq string 4 6) :radix 16)))

(dc:defconfig *background-color* (make-color :r 256 :g 256 :b 256)
  :documentation "the background color for my app"
  :typespec 'color
  :coercer (lambda (value)
             (handler-case (parse-color-from-string value)
               (error () value)))
  :tags '("color" "background"))

Setv

Now, somewhere in the users ~/.myapp.d/init, they want to set the background color to black. Lets look at three examples of that:

(dc:setv *background-color* (make-color :r 0 :g 0 :b 0))
(dc:setv *background-color* "000000")
(dc:setv *background-color* "i dont know what this user is thinking!")

The first of these works without a hitch; setv determines that it is a valid value as per the typespec the user provided, and sets *background-color* to black.

The second of these would fail if we hadnt provided a coercer, but as we did, and it knows how to handle color strings, we generate a color from the color string and *background-color* gets set to black.

The third of these is also a string, but its impossible to parse a color from it. Assuming parse-color-from-string errors out on invalid strings, we return value and signal an error; *background-color* remains white.

With-atomic-setv

Lets look at an example of with-atomic-setv. We will define a bounded number variable, and then try setting it while signalling various errors.

(dc:defconfig *bounded-number* 0
  :typespec '(integer 0 10)
  :coercer (lambda (x)
             (if (stringp x)
                 (handler-case (parse-integer x)
                   (error () x))
                 x)))

(defun compute-something-that-signals-an-error ()
  (error "we encountered an error, oh no!"))

(dc:with-atomic-setv ()
  (dc:setv *bounded-number* 1)
  (dc:setv *bounded-number* 50))

(dc:with-atomic-setv ()
  (dc:setv *bounded-number* 1)
  (compute-something-that-signals-an-error)
  (dc:setv *bounded-number* 2))

(dc:with-atomic-setv (:handle-conditions dc:config-error)
  (dc:setv *bounded-number* 1)
  (compute-something-that-signals-an-error)
  (dc:setv *bounded-number* 2))

The first of the calls to with-atomic-setv first sets *bounded-number* to 1, and then encounters an error when trying to set it to 50. It catches that error and resets *bounded-number* to 0, the value *bounded-number* had before the call to with-atomic-setv.

The second of these first sets *bounded-number* to 1, and then an error is signalled by (compute-something-that-signals-an-error). It catches this error and resets *bounded-number* to 0.

The third of these first sets *bounded-number* to 1, and then an error is signalled that it is not set up to handle; it will only catch errors of type config-error. Whether or not it attempts to set *bounded-number* to 2 is determined by what handlers and restarts are set up around the error. If there a restart is chosen that doesnt unwind the stack then *bounded-number* will be set to 2, but if there is a non-local transfer of control to a point outside of with-atomic-setv then *bounded-number* will remain set to 1. This is the only way to escape with-atomic-setv that leaves things in a partially configured state. Lets look at an example of this that would end up with *bounded-number* being 2:

(defun compute-something-that-signals-an-error ()
  (restart-case (error "we encountered an error, oh no!")
    (continue () nil)))

(handler-bind ((error
                 (lambda (c)
                   (declare (ignore c))
                   (when (find-restart 'continue)
                     (invoke-restart 'continue)))))
  (dc:with-atomic-setv (:handle-conditions dc:config-error)
    (dc:setv *bounded-number* 1)
    (compute-something-that-signals-an-error)
    (dc:setv *bounded-number* 2)))

Controlling Permissiveness

By setting *setv-permissiveness* one can control how setv handles missing configuration objects. It can be set to one of the following values:

  • :strict - Signal all errors as they occur. This is the default behavior
  • :greedy - When unable to find a configuration object in the specified database, search in all databases for a matching configuration object, using the first one encountered.
  • :permissive - When a configuration object isnt found, set the variable to the value.
  • :greedy+permissive - When a configuration object isnt found, search for one as per :greedy. If one still isnt found, set the variable to the value.

Quirks, Oddities and Limitations

There are a few places in defconfig that arent naturally intuitive.

Setv and macros

Setv wont work with macros that expand into something else to be set in the same way setf does. Example:

(defconfig *var* nil)
(defmacro var () '*var*)
(setf (var) 1) ; works
(setv (var) 2) ; tries to find config for the accessor var, not the variable *var*

Psetv

The macro psetv is a setv equivalent of psetf. However, while bindings are “preserved” throughout the form, if an error occurs and there is a non-local transfer of control, any places being set after the error will not be set. An example from the test suite:

(defconfig-minimal *a* 'a
  :typespec 'symbol)

(defconfig-minimal *b* "b"
  :typespec 'string)

(defconfig-minimal *c* 'c
  :typespec 'symbol)

(psetv *a* *c*
       *b* *a*
       *c* *a*)

If one enters this in a repl, an error condition will be signalled upon trying to set *b* to ~’a~, and if one chooses to abort (via q, or sly-db-abort) then *c* will retain the value ~’c~, and *b* ~”b”~.

With-atomic-setv

The star variant of with-atomic-setv has a quirk in that places get evaluated multiple times if one resets, while both variants evaluate accessors multiple times. Some code to demonstrate:

(with-atomic-setv ()
  (setv (accessor *myvar*) 0)
  …)

(with-atomic-setv* ()
  (setv (accessor *myvar*) 0)
  …)

Both of these will evaluate (accessor *myvar*) multiple times depending on whether it gets reset or not.

(with-atomic-setv ()
  (setv (accessor (progn (incf *counter*)
			   *myvar*))
	  0)
  …)

(with-atomic-setv* ()
  (setv (accessor (progn (incf *counter*)
			   *myvar*))
	  0)
  …)

In the above example, the first of these will evaluate (progn (incf *counter*) *myvar*) once and only once, while the second will evaluate (progn (incf *counter*) *myvar*) once if there is no reset, but twice if there is a reset. Both version of this macro will evaluate the accessor multiple times. Another way of putting it is to say that with-atomic-setv* is symmetrical - that is to say, upon resetting every call to setv will have a matching reset. In contrast, with-atomic-setv will only reset a place if it hasn’t already been reset.

A Note About Reset-Place

The macro reset-place (and by extension the function reset-computed-place) could be a little confusing. It takes a place, and resets it to its default value. However if previous-value is true, then it resets to the previous value instead. Before setting, it checks if the current value is eql to the value to reset to (this can be controlled with already-reset-test) and if it is it isnt reset as it would have no effect. If it isnt, we both reset the place, AND set the previous-value slot to the (now no longer) current value. thusly, if the default value is a, previous value is b, and current value is c, and we reset to the default value, we will have a default of a, previous of c, and current of a. If we had instead reset to the previous value, we effectively swap the previous and current values. Furthermore, we cannot reset accessor places.

Defconfig and CLISP

Many distributions package an older version of CLISP upon which the defconfig testing dependency :fiveam wont load. CLISP version 2.49.92 and higher is known to work, and can be obtained from the 2.50 branch on gitlab. At the time of writing the master branch is v2.49.93+.

Macros

Define-variable-config

define-variable-config place default-value &key validator typespec coercer documentation db tags regen-config => config-info

The define-variable-config macro generates a config info object and registers it in a database.

  • Side Effects
    • Causes config-info to be places into db
    • Any side effects of calling validator on default-value, when validator is provided.
  • Arguments and Values
    • place - a symbol denoting a dynamic variable.
    • default-value - the default value for place. Must conform to validator or typespec when provided.
    • validator - a function of one argument returning true or nil. May not be provided alongside typespec.
    • typespec - a type specifier denoting valid types for place. May not be provided alongside validator
    • coercer - a function of one argument used to attempt to coerce its argument to a valid value. Will only be called on invalid values.
    • regen-config - when true, regenerate the configuration object regarless of its pre-existence
    • db - the database to register config-info in.
    • tags - a set of tags used when searching for a configuration object

Define-accessor-config

define-accessor-config place &key validator typespec coercer documentation db tags regen-config => config-info

The define-accessor-config macro generates a config info object and registers it in a database.

  • Side Effects
    • Causes config-info to be places into db
  • Arguments and Values
    • place - a symbol denoting a dynamic variable.
    • validator - a function of one argument returning true or nil. May not be provided alongside typespec.
    • typespec - a type specifier denoting valid types for place. May not be provided alongside validator
    • coercer - a function of one argument used to attempt to coerce its argument to a valid value. Will only be called on invalid values.
    • regen-config - when true, regenerate the configuration object regarless of its pre-existence
    • db - the database to register config-info in.
    • tags - a set of tags used when searching for a configuration object

Defconfig

defconfig place &rest args => config-info

The defconfig macro wraps around the define-*-config macros. When place is a symbol, it expands into a call to define-variable-config, as well as a call to either defparameter or defvar. When place is a symbol one additional key argument is accepted: :reinitialize. When true, a defparameter form is generated.

  • Side Effects:
    • Causes config-info to be placed into db
    • May modify place
    • May cause place to be defined as a dynamic variable
    • Any side effects of running validator

Setv

setv {place value}* :db => result The setv macro expands into multiple calls to %%setv, which validates a value before setting the place to it. It functions the same as setf, but accepts the keyword :db to specify a database other than the default one provided by defconfig. Returns the final value.

  • Side Effects:
    • Any side effects of evaluating a value. Place/value pairs are evaluated sequentially. If a value is not valid, no further values will be processed.
    • Causes place to be set to value

Psetv

psetv {place value}* :db => result The psetv macro expands into multiple calls to %%setv, the same as setv, but differs in that all values are computed before setting, giving the illusion of setting in parallel (similar to psetf).

  • Side Effects:
    • Any side effects of evaluating a value. Place/value pairs are evaluated sequentially. If a value is not valid, no further values will be processed.
    • Causes place to be set to value

With-atomic-setv/*

with-atomic-setv (errorp handle-conditions db) form* => results There are two versions of this macro: with-atomic-setv and with-atomic-setv*. The former tracks places and values purely at runtime, while the latter tracks places at macroexpansion time and values at runtime.

The with-atomic-setv macro resets any places set using setv to the value it held before the call to with-atomic-setv, when a condition is encountered. One can specify whether to re-signal the condition or not with :errorp. If :errorp is nil a warning will be issued on encountering a handled condition and the condition will be returned. Re-signalled conditions are wrapped in the condition setv-wrapped-error. One can specify which conditions to handle with :handle-conditions, which accepts an (unquoted) type specifier. One can handle no conditions by passing (or), though that defeats the purpose of with-atomic-setv. The default database to use for all calls to setv occuring within form* can be controleld with :db. It defaults to defconfig:*default-db*.

An example:

(with-atomic-setv (:errorp nil)
  (error "hello")
  "return string")

WARNING: WITH-ATOMIC-SETV encountered the error
#<SIMPLE-ERROR "hello" {address}>
and reset.
=> #<SIMPLE-ERROR "hello" {address}>

(with-atomic-setv (:errorp nil :handle-conditions config-error)
  (error "hello")
  "return string")

drops into the debugger

(with-atomic-setv (:errorp nil)
  (warn "hello")
  "return string")

WARNING: hello
=> "return string"

(with-atomic-setv (:errorp nil :handle-conditions (or error warning))
  (warn "hello")
  "return string")

WARNING: hello
=> #<SIMPLE-WARNING "hello" {address}>

Define-defconfig-db

define-defconfig-db var key &key parameter if-exists doc

The define-defconfig-db macro defines a new dynamic variable containing a defconfig database and stores that database internally such that it can be referenced via key. All databases used with defconfig should be created using this macro. The key argument parameter defaults to true, and controls whether the database is defined using defvar or defparameter. Variable documentation is added via the doc key argument. When a key already denotes a defconfig database, an error will be signalled. This can be handled by setting if-exists to :use or redefine, to either use the existing database or redefine the database respectively. When if-exists is nil the error will propogate up to the user.

Help Wanted

Currently, Travis is being used for CI. However, these builds sometimes fail for unknown reasons unrelated to the defconfig test suite. It would be nice to be able to detect these failures and re-run the job upon encountering them.

About

A customizer for common lisp dynamic variables

License:GNU Lesser General Public License v2.1


Languages

Language:Common Lisp 100.0%