What is Universal-Config?
This library tries to provide a layer for configuration files and storage that should be able to map any type of object to any configuration format and back.
This library tries to provide a layer for configuration files and storage that should be able to map any type of object to any configuration format and back.
Accessing configuration works through the CONFIG-TREE
function.
(uc:config-tree :access "a path" 2)
CONFIG-TREE
internally uses the ACCESS
function that tries to generically dispatch according to accessor and current object. By default it understands hash-tables, arrays, sequences, lists and slots on standard-objects. Support for alists and plists is not provided, as it isn't possible to properly distinguish them in a generic fashion. CONFIG-TREE
and ACCESS
are setf-able places. The former will try to augment missing objects in the configuration automatically upon setf if *AUGMENT-MISSING-PLACES*
is non-NIL.
Saving and loading the configuration works with SAVE-CONFIGURATION
and LOAD-CONFIGURATION
respectively.
(uc:save-configuration #p"~/test.lisp")
Depending on what format is available (by default lisp and json), you can pass the format argument:
(uc:save-configuration #p"~/test.json" :format :json)
Due to having to save arbitrary types, universal-config has to serialize objects into a common structure. Depending on the amount of types the format supports, this will produce more or less gross output. Standard lisp format makes an effort to keep it readable though.
Universal-Config can be extended in three ways, the first being object accessors. In order to make Universal-Config support your own data structures, define methods for ACCESS
and (SETF ACCESS)
. The ACCESS
method should always return two values, the first being the actual value or the supplied default value and the second being T if the place was found or NIL otherwise.
(defmethod uc:access ((list list) (accessor keyword) &optional default)
(let ((return (assoc accessor list)))
(if return (values return T) (values default NIL))))
(defmethod (setf uc:access) (value (list list) (accessor keyword))
(setf (cdr (assoc accessor list)) value))
Secondly, to make extra types like classes be serializable, you have to extend SERIALIZE
and DESERIALIZE
. The easiest way to do this is in a fashion similar to this:
(uc:define-serializer (my-class instance T)
(make-array 2 :initial-contents (list (uc:serialize (first-slot instance)) (uc:serialize (other-slot instance)))))
The last optional argument to DEFINE-SERIALIZER
is a minor optimization switch that should be set to T if the returned object is a vector.
(uc:define-deserializer (my-class vector T)
(make-instance 'my-class :first-slot (uc:deserialize (aref vector 0)) :other-slot (uc:deserialize (aref vector 1))))
However, adding de/serializers for classes can be eased with the shorthand macro DEFINE-CLASS-DE/SERIALIZER
:
(uc:define-class-de/serializer my-class first-slot other-slot)
When serializing, the output type should always be either an array, list, hash-map, string or number. Strings are handled specially though to allow for different types to be serialized into strings for output formats that don't support their canonical representations. The type the string is deserialized into is determined by its first character. Standard strings should therefore be preceded by #\- . If you have a data type that would fit better into a single string, you can define new de/serializing constructs for that through DEFINE-STRING-SERIALIZER
and DEFINE-STRING-DESERIALIZER
. By default string de/serializers for symbols, integers, ratios, floats and complex numbers are defined.
The last part is defining your own external formats. Universal-Config comes with two formats, LISP
and JSON
. Since differing formats support different types, there's a few switches that control the serialization process: *SERIALIZE-SYMBOLS*
*SERIALIZE-NUMBERS*
*SERIALIZE-HASH-TABLES*
*SERIALIZE-LISTS*
. The first two are string-serializer switches, the latter two decide whether these types should be wrapped in a vector, since most output formats don't support vectors and lists as different types, nor the different hash-table tests.
(uc:define-save-format my-format (stream object)
(my-printer stream (serialize object)))
(uc:define-load-format my-format (stream)
(deserialize (my-reader stream)))
Due to the way formats differ wildly, Universal-Config cannot assist further than providing the de/serialization facilities. It should not be too difficult to plug this into an existing format library though.
Universal-Config is licensed under the Artistic License 2.0 and ©2013 TymoonNET/NexT, Nicolas Hafner.
This library can be obtained via git on https://github.com/Shinmera/universal-config.git. For questions, patches or suggestions, please contact me via email or write a github issue.
*AUGMENT-MISSING-PLACES*
If set to non-NIL, (SETF (CONFIG-TREE ..) ..) will attempt to augment missing places.
*CONFIG*
The global configuration storage variable.
*FALLBACK-DESERIALIZER*
Fallback function used when no deserializer method matched. Useful for applying deserializers specific to the input format.
*FALLBACK-SERIALIZER*
Fallback function used when no serializer method matched. Useful for applying serializers specific to the output format.
*OUTPUT-FORMAT*
The default output format to use.
*SERIALIZE-HASH-TABLES*
Whether to serialize hash-tables into a vector and a hash-table. This is necessary for most output formats as there is no differentiation made between hash-table tests. Serializing them will ensure that the proper test can be restored upon deserializing.
*SERIALIZE-LISTS*
Whether to serialize lists into vector representation. This is necessary for most output formats as there are no two representations of list- or vector-like structures.
*SERIALIZE-NUMBERS*
Whether to serialize numbers into string representation. This is necessary for many output formats as they do not support the variety of number types lisp provides (ratios, floats, complex numbers).
*SERIALIZE-SYMBOLS*
Whether symbols should be serialized into string representation. Note that symbol plists are not serialized into strings.
ACCESS
(CONFIG-OBJECT ACCESSOR &OPTIONAL DEFAULT)
Universal object accessor. Returns two values, the first being the accessed value or the supplied default, the second being T if the requested place was found or NIL if the default is returned.
ACCESSOR
(CONDITION)
AUGMENTING-PLACE
Warning condition signalled when a place is augmented automatically.
CONFIG-TREE
(&REST ACCESSORS)
Retrieve a value from the configuration.
DEFINE-CLASS-DE/SERIALIZER
(CLASS &REST SLOTDEFS)
Shorthand macro to define de/serializer methods for a class and the specified slots. CLASS --- A class type. SLOTDEFS ::= SLOTDEF* SLOTDEF ::= SLOT-SYMBOL | (SLOT-SYMBOL INITARG-SYMBOL)
DEFINE-DESERIALIZER
((OBJECT-TYPE OBJECT-VAR &OPTIONAL EXPECT-VECTOR) &BODY BODY)
Define a new OBJECT-TYPE to deserialize into a usable representation. If EXPECT-VECTOR is non-NIL, the bound OBJECT-VAR will be of type VECTOR.
DEFINE-LOAD-FORMAT
(NAME (STREAMVAR) &BODY BODY)
Define a format of NAME to load an object from a stream.
DEFINE-SAVE-FORMAT
(NAME (STREAMVAR OBJECTVAR) &BODY BODY)
Define a format of NAME to save an object to a stream.
DEFINE-SERIALIZER
((OBJECT-TYPE OBJECT-VAR &OPTIONAL RETURN-VECTOR) &BODY BODY)
Define an OBJECT-TYPE to be serialized. The expected value of this function should be one of HASH-TABLE, VECTOR, STRING or NUMBER. If RETURN-VECTOR is non-NIL, the object returned should be of type VECTOR.
DEFINE-STRING-SERIALIZER
((IDENT-CHAR OBJECT-TYPE OBJECT-VAR) &BODY BODY)
Defines an OBJECT-TYPE to be serialized to a string. To discern string serialized objects, an IDENT-CHAR is needed.
DESERIALIZE
(OBJECT)
Deserialize an OBJECT into a usable configuration object.
ESCAPE
(STRING &OPTIONAL (CHAR :))
Escape all instances of CHAR in the string that match CHAR with a backslash.
INEXISTENT-PLACE
Error condition signalled when attempting to set an inexistent place.
LOAD-CONFIGURATION
(PATH &KEY (FORMAT *OUTPUT-FORMAT*) (IF-DOES-NOT-EXIST ERROR))
Load the configuration from PATH with the given FORMAT.
MAKE-CONTAINER
(ACCESSOR)
Attempts to create a fitting container for an accessor.
OBJECT
(CONDITION)
SAVE-CONFIGURATION
(PATH &KEY (FORMAT *OUTPUT-FORMAT*) (OBJECT *CONFIG*))
Save the configuration OBJECT to PATH using the given FORMAT.
SERIALIZE
(OBJECT)
Serialize the given object recursively into a format that is ready for outputting.
SET-CONFIG-TREE
(ACCESSORS VALUE)
Sets the place indicated by the ACCESSORS list to VALUE if possible. See (SETF CONFIG-TREE)
SPLIT-ESCAPED
(STRING &OPTIONAL (CHAR :))
Split the string by CHAR minding backslash escaped instances.
UNESCAPE
(STRING &OPTIONAL (CHAR :))
Unescape all backslash escaped instances of CHAR.
WITH-CONFIGURATION
((CONFIGURATION) &BODY BODY)
Establishes a configuration context.