Subject: [bug#52600] [PATCH] doc: Document (gnu services configuration).

From: Xinglu Chen <[email-masked]>
Date: Sat, 18 Dec 2021 16:12:38 +0100

* guix.texi (Complex Configurations): New node.
---
Hi!

This patch documents the complex beast that is the (gnu services
configuration) module.  I only documented the things that existed before
the Guix Home merge, though.  There were a lot of things to document, and
I hope that my explanations aren’t too confusing (It took me a while to
wrap my head around all of this).  :-)

What is still missing is some kind of style guide for writing Guix
services:  When should one use (gnu services configuration) vs plain
(guix records)?  Should we try to create bindings for all config options
or should we provide an “escape hatch” for users?

I would personally prefer if most (if not all) services were written
using (gnu services configuration), but I don’t really think refactoring
existing services would really be worth it.  But that’s another discussion.

 doc/guix.texi | 372 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 372 insertions(+)

diff --git a/doc/guix.texi b/doc/guix.texi
index aca88cdada..79b87d2eac 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -383,6 +383,7 @@
 * Service Types and Services::  Types and services.
 * Service Reference::           API reference.
 * Shepherd Services::           A particular type of service.
+* Complex Configurations::      Defining bindgs for complex configurations.
 
 Installing Debugging Files
 
@@ -35656,6 +35657,7 @@
 * Service Types and Services::  Types and services.
 * Service Reference::           API reference.
 * Shepherd Services::           A particular type of service.
+* Complex Configurations::      Defining bindgs for complex configurations.
 @end menu
 
 @node Service Composition
@@ -36389,6 +36391,376 @@
 This service represents PID@tie{}1.
 @end defvr
 
+@node Complex Configurations
+@subsection Complex Configurations
+@cindex complex configurations
+Some programs might have rather complex configuration files or formats,
+and to make it easier to create Scheme bindings for these configuration
+files, you can use the utilities defined in the @code{(gnu services
+configuration)} module.
+
+The main utility is the @code{define-configuration} macro, which you
+will use to define a Scheme record type (@pxref{Record Overview,,,
+guile, GNU Guile Reference Manual}).  The Scheme record will be
+serialized to a configuration file by using @dfn{serializers}, which are
+procedures that take some kind of Scheme value and returns a
+G-expression (@pxref{G-Expressions}), which should, once serialized to
+the disk, return a string.  More details are listed below.
+
+@deffn {Scheme Syntax} define-configuration @var{name} @var{clause1} @
+@var{clause2} ...
+Create a record type named @code{@var{name}} that contains the
+fields found in the clauses.
+
+A clause can have one the following forms
+
+@example
+(@var{field-name}
+ (@var{type} @var{default-value})
+ @var{documentation})
+ 
+(@var{field-name}
+ (@var{type} @var{default-value})
+ @var{documentation}
+ @var{serializer})
+
+(@var{field-name}
+ (@var{type})
+ @var{documentation})
+
+(@var{field-name}
+ (@var{type})
+ @var{documentation}
+ @var{serializer})
+@end example
+
+@var{field-name} is an identifier that denotes the name of the field in
+the generated record.
+
+@var{type} is the type of the value corresponding to @var{field-name};
+since Guile is untyped, a predicate
+procedure---@code{@var{type}?}---will be called on the value
+corresponding to the field to ensure that the value is of the correct
+type.  This means that if say, @var{type} is @code{package}, then a
+procedure named @code{package?} will be applied on the value to make
+sure that it is indeed a @code{<package>} object.
+
+@var{default-value} is the default value corresponding to the field; if
+none is specified, the user is forced to provide a value when creating
+an object of the record type.
+
+@c XXX: Should these be full sentences or are they allow to be very
+@c short like package synopses?
+@var{documentation} is a string formatted with Texinfo syntax which
+should provide a description of what setting this field does.
+
+@var{serializer} is the name of a procedure which takes two arguments,
+the first is the name of the field, and the second is the value
+corresponding to the field.  The procedure should return a string or
+G-expression (@pxref{G-Expressions}) that represents the content that
+will be serialized to the configuration file.  If none is specified, a
+procedure of the name @code{serialize-@var{type}} will be used.
+
+A simple serializer procedure could look like this.
+
+@lisp
+(define (serialize-boolean field-name value)
+  (let ((value (if value "true" "false")))
+    #~(string-append #$field-name #$value)))
+@end lisp  
+
+In some cases multiple different configuration records might be defined
+in the same file, but their serializers for the same type might have to
+be different, because they have different configuration formats.  For
+example, the @code{serialize-boolean} procedure for the Getmail service
+would have to be different for the one for the Transmission service.  To
+make it easier to deal with this situation, one can specify a serializer
+prefix by using the @code{prefix} literal in the
+@code{define-configuration} form.  This means that one doesn't have to
+manually specify a custom @var{serializer} for every field.
+
+@lisp
+(define (foo-serialize-string field-name value)
+  @dots{})
+
+(define (bar-serialize-string field-name value)
+  @dots{})
+  
+(define-configuration foo-configuration
+  (label
+   (string)
+   "The name of label.")
+  (prefix foo-))
+
+(define-configuration bar-configuration
+  (ip-address
+   (string)
+   "The IPv4 address for this device.")
+  (prefix bar-))
+@end lisp
+
+However, in some cases you might not want to serialize any of the values
+of the record, to do this, you can use the @code{no-serialization}
+literal.  There is also the @code{define-configuration/no-serialization}
+macro which is a shorthand of this.
+
+@lisp
+;; Nothing will be serialized to disk.
+(define-configuration foo-configuration
+  (field
+   (string "test")
+   "Some documentation.")
+  (no-serialization))
+
+;; The same thing as above.
+(define-configuration/no-serialization bar-configuration
+  (field
+   (string "test")
+   "Some documentation."))
+@end lisp   
+@end deffn
+
+@deffn {Scheme Syntax} define-maybe @var{type}
+Sometimes a field should not be serialized if the user doesn’t specify a
+value.  To achieve this, you can use the @code{define-maybe} macro to
+define a ``maybe type''; if the value of a maybe type is set to the
+@code{disabled}, it will not be serialized.
+
+When defining a ``maybe type'', the corresponding serializer for the
+regular type will be used by default.  For example, a field of type
+@code{maybe-string} will be serialized using the @code{serialize-string}
+procedure by default, you can of course change this by specifying a
+custom serializer procedure.  Likewise, the type of the value would have
+to be a string, unless it is set to the @code{disabled} symbol.
+
+@lisp
+(define-maybe string)
+
+(define (serialize-string field-name value)
+  @dots{})
+
+(define-configuration baz-configuration
+  (name
+   ;; Nothing will be serialized by default.  If set to a string, the
+   ;; `serialize-string' procedure will be used to serialize the string.
+   (maybe-string 'disabled)
+   "The name of this module."))
+@end lisp
+
+Like with @code{define-configuration}, one can set a prefix for the
+serializer name by using the @code{prefix} literal.
+
+@lisp
+(define-maybe integer
+  (prefix baz-))
+
+(define (baz-serialize-interger field-name value)
+  @dots{})
+@end lisp
+
+There is also the @code{no-serialization} literal, which when set means
+that no serializer will be defined for the ``maybe type'', regardless of
+its value is @code{disabled} or not.
+@code{define-maybe/no-serialization} is a shorthand for specifying the
+@code{no-serialization} literal.
+
+@lisp
+(define-maybe/no-serialization symbol)
+
+(define-configuration/no-serialization test-configuration
+  (mode
+   (maybe-symbol 'disabled)
+   "Docstring."))
+@end lisp
+@end deffn
+
+@deffn {Scheme Procedure} serialize-configuration @var{configuration} @
+@var{fields}
+Return a G-expression that contains the values corresponding to the
+@var{fields} of @var{configuration}, a record that has been generated by
+@code{define-configuration}.  The G-expression can then be serialized to
+disk by using something like @code{mixed-text-file}.
+@end deffn
+
+@deffn {Scheme Procedure} validate-configuration @var{configuration}
+@var{fields}
+Type-check @var{fields}, a list of field names of @var{configuration}, a
+configuration record created by @code{define-configuration}.
+@end deffn
+
+@deffn {Scheme Procedure} empty-serializer @var{field-name} @var{value}
+A serializer that just returns an empty string.  The
+@code{serialize-package} procedure is an alias for this.
+@end deffn
+
+Once you have defined a configuration record, you will most likely also
+want to document it so that other people know to use it.  To help with
+that, there are two procedures, both of which are documented below.
+
+@deffn {Scheme Procedure} generate-documentation @var{documentation} @
+@var{documentation-name}
+Generate a Texinfo fragment from the docstrings in @var{documentation},
+a list of @code{(@var{label} @var{fields} @var{sub-documentation} ...)}.
+@var{label} should be a symbol and should be the name of the
+configuration record.  @var{fields} should be a list of all the fields
+available for the configuration record.
+
+@var{sub-documentation} is a @code{(@var{field-name}
+@var{configuration-name})} tuple.  @var{field-name} is the name of the
+field which takes another configuration record as its value, and
+@var{configuration-name} is the name of that configuration record.
+
+@var{sub-documentation} is only needed if there are nested configuration
+records.  For example, the @code{getmail-configuration} record
+(@pxref{Mail Services}) accepts a @code{getmail-configuration-file}
+record in one of its @code{rcfile} field, therefore documentation for
+@code{getmail-configuration-file} is nested in
+@code{getmail-configuration}.
+
+@lisp
+(generate-documentation
+  `((getmail-configuration ,getmail-configuration-fields
+     (rcfile getmail-configuration-file))
+    @dots{})
+  'getmail-configuration)
+@end lisp
+
+@var{documentation-name} should be a symbol and should be the name of
+the configuration record.
+
+@end deffn
+
+@deffn {Scheme Procedure} configuration->documentation
+@var{configuration-symbol}
+Take @var{configuration-symbol}, the symbol corresponding to the name
+used when defining a configuration record with
+@code{define-configuration}, and print the Texinfo documentation of its
+fields.  This is useful if there aren’t any nested configuration records
+since it only prints the documentation for the top-level fields.
+@end deffn
+
+As of right now, there is no automated way to generate documentation for
+and configuration records and put them in the manual.  Instead, every
+time you make a change to the docstrings of a configuration record, you
+have to manually call @code{generate-documentation} or
+@code{configuration->documentation}, and paste the output into the
+@file{doc/guix.texi} file.
+
+@c TODO: Actually test this
+Below is an example of a record type created using
+@code{define-configuration} and friends.
+
+@lisp
+(use-modules (gnu services)
+             (guix gexp)
+             (gnu services configuration)
+             (srfi srfi-26)
+             (srfi srfi-1))
+
+;; Turn field names, which are Scheme symbols into strings
+(define (uglify-field-name field-name)
+  (let ((str (symbol->string field-name)))
+    ;; field? -> is-field
+    (if (string-suffix? "?" str)
+        (string-append "is-" (string-drop-right str 1))
+        str)))
+
+(define (serialize-string field-name value)
+  #~(string-append #$(uglify-field-name field-name) " = " #$value "\n"))
+
+(define (serialize-integer field-name value)
+  (serialize-string field-name (number->string value)))
+
+(define (serialize-boolean field-name value)
+  (serialize-string field-name (if value "true" "false")))
+
+(define (serialize-contact-name field-name value)
+  #~(string-append "\n[" #$value "]\n"))
+
+(define (list-of-contact-configurations? lst)
+  (every contact-configuration? lst))
+
+(define (serialize-list-of-contact-configurations field-name value)
+  #~(string-append #$@@(map (cut serialize-configuration <>
+                                contact-configuration-fields)
+                           value)))
+
+(define (serialize-contacts-list-configuration configuration)
+  (mixed-text-file
+   "contactrc"
+   #~(string-append "[Owner]\n"
+                    #$(serialize-configuration
+                       configuration contacts-list-configuration-fields))))
+
+(define-maybe integer)
+(define-maybe string)
+
+(define-configuration contact-configuration
+  (name
+   (string)
+   "The name of the contact."
+   serialize-contact-name)
+  (phone-number
+   (maybe-integer 'disabled)
+   "The person's phone number.")
+  (email
+   (maybe-string 'disabled)
+   "The person's email address.")
+  (married?
+   (boolean)
+   "Whether the person is married."))
+
+(define-configuration contacts-list-configuration
+  (name
+   (string)
+   "The name of the owner of this contact list.")
+  (email
+   (string)
+   "The owner's email address.")
+  (contacts
+   (list-of-contact-configurations '())
+   "A list of @@code@{contact-configuation@} records which contain
+information about all your contacts."))
+@end lisp
+
+A contacts list configuration could then be created like this:
+
+@lisp
+(define my-contacts
+  (contacts-list-configuration
+   (name "Alice")
+   (email "alice@@example.org")
+   (contacts
+    (list (contact-configuration
+           (name "Bob")
+           (phone-number 1234)
+           (email "bob@@gnu.org")
+           (married? #f))
+          (contact-configuration
+           (name "Charlie")
+           (phone-number 0000)
+           (married? #t))))))
+@end lisp
+
+After serializing the configuration to disk, the resulting file would
+look like this:
+
+@example
+[owner]
+name = Alice
+email = alice@@example.org
+
+[Bob]
+phone-number = 1234
+email = bob@@gnu.org
+is-married = false
+
+[Charlie]
+phone-number = 0
+is-married = true
+@end example
+
+
 @node Home Configuration
 @chapter Home Configuration
 @cindex home configuration

base-commit: 6061540e30269934dae3395ab9fc1b905a414247
-- 
2.33.1