Subject: [bug#52698] [RFC PATCH] doc: Add Writing Service Configuration section.

From: Andrew Tropin <[email-masked]>
Date: Tue, 21 Dec 2021 13:21:20 +0300

* guix.texi (Writing Service Configuration): New section.
---
After reading the source code of different system services and implementing a
few of home services I decided to write down most important tips for
implementing guix service configurations.  I belive having such a guideline
can simplify the development of new services and configurations for them, as
well as keeping those implementations consistent, which will simplify the life
for users too because they won't need to learn a different configuration
approaches for different services.

This section is not a final document, but a starting point for discussion and
further extension of the guideline.  Feel free to raise a question, point to a
mistake, make a suggestion or propose an idea.

Best regards,
Andrew Tropin

 doc/guix.texi | 209 +++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 205 insertions(+), 4 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index 333cb4117a..a48fb0e2b7 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -35652,10 +35652,11 @@ them in an @code{operating-system} declaration.  But how do we define
 them in the first place?  And what is a service anyway?
 
 @menu
-* Service Composition::         The model for composing services.
-* Service Types and Services::  Types and services.
-* Service Reference::           API reference.
-* Shepherd Services::           A particular type of service.
+* Service Composition::            The model for composing services.
+* Service Types and Services::     Types and services.
+* Service Reference::              API reference.
+* Shepherd Services::              A particular type of service.
+* Writing Service Configurations:: A guideline for writing guix services.
 @end menu
 
 @node Service Composition
@@ -35851,6 +35852,206 @@ There can be only one instance of an extensible service type such as
 Still here?  The next section provides a reference of the programming
 interface for services.
 
+@node Writing Service Configurations
+@subsection Writing Service Configurations
+
+There are a lot of system and home services already written, but from
+time to time it's necessary to write one more.  This section contains
+tips for simplifying this process, and should help to make service
+configurations and their implementations more consistent.
+
+@quotation Note
+If you find any exceptions or patterns missing in this section, please
+send a patch with additions/changes to @email{guix-devel@@gnu.org}
+mailing list or just start a discussion/ask a question.
+@end quotation
+
+@subsubheading Configuration Itself
+
+As we know from previous section a guix service can accept a value and
+be extended with additional values by other services.  There are some
+cases, when the service accepts a list of pairs or some other values for
+example @code{console-font-service-type} accepts list of pairs (tty and
+font name/file) or @code{etc-service-type} accepts list of lists
+(resulting file name and file-like object), those services are kinda
+special, they are an intermediate helpers doing auxiliary work.
+
+However, in most cases a guix service is wrapping some software, which
+consist of package or a few packages, and configuration file or files.
+Therefore, the value for such service is quite complicated and it's hard
+to represent it with a just list or basic data type, in such cases we
+use a record.  Each such record have -configuration suffix, for example
+@code{docker-configuration} for @code{docker-service-type} and a few
+different fields helping to customize the software.  Configuration
+records for home services also have a @code{home-} prefix in their name.
+
+There is a module @code{gnu service configuration}, which contains
+helpers simplifying configuration definition process.  Take a look at
+@code{gnu services docker} module or grep for
+@code{define-configuration} to find usage examples.
+
+@c Provide some examples, tips, and rationale behind @code{gnu service
+@c configuration} module.
+
+After a configuration record properly named and defined let's discuss
+how to name and define fields, and which approach to use for
+implementing the serialization code related to them.
+
+@subsubheading Configuration Record Fields
+
+@enumerate
+@item
+It's a good idea to have a field/fields for specifying package/packages
+being installed for this service.  For example
+@code{docker-configuration} has @code{docker}, @code{docker-cli},
+@code{containerd} fields.  Sometimes it make sense to make a field,
+which accepts a list of packages for cases, where an arbitrary list of
+plugins can be passed to the configuration.  There are some services,
+which provide a field called @code{package} in their configuration,
+which is ok, but the way it done in @code{docker-configuration} is more
+flexible and thus preferable.
+
+@item
+Fields for configuration files, should be called the same as target
+configuration file name, but in kebab-case: bashrc for bashrc,
+bash-profile for bash_profile, etc.  The implementation for such fields
+will be discussed in the next subsubsection.
+
+@item
+Other fields in most cases add some boilerplates/reasonable defaults to
+configuration files, turns on/off installation of some packages or
+provide other custom behavior.  There is no any special requirements or
+recommendations here, but it's necessary to make it possible to disable
+all the effects of such fields to provide a user with an empty
+configuration and let them generate it from scratch with only field for
+configuration file.
+
+@end enumerate
+
+@subsubheading Fields for Configuration Files
+
+The field should accept a datastructure (preferably a combination of
+simple lists, alists, vectors, gexps and basic data types), which will
+be serialized to target configuration format, in other words it should
+provide an alternative lisp syntax, which can be later translated to
+target one, like SXML for XML.  Such approach is quite flexible and
+simple, it requires to write serializer once for one configuration
+format and can be reused multiple times in different guix services.
+
+Let's take a look at JSON: we implement serialization function, which
+converts vectors to arrays, alists to objects (AKA dictionaries or
+associative arrays), numbers to numbers, gexps to the strings, file-like
+objects to the strings, which contains the path to the file in the
+store, @code{#t} to @code{true} and so on, and now we have all apps
+using JSON and YAML as a format for configurations covered.  Maybe some
+fine-tunning will be needed for particular application, but the primary
+serilalization part is already finished.
+
+The pros and cons of such approach is inherited from open-world
+assumption.  It doesn't matter if underlying applications provides new
+configuration options, we don't need to change anything in service
+configuration and its serialization code, it will work perfectly fine,
+on the other hand it harder to type check and structure check
+``compile-time'', and we can end up with configuration, which won't be
+accepted by target application cause of unexisting, misspelled or
+wrongly-typed options.  It's possible to add those checks, but we will
+get the drawbacks of closed-world assumption: we need to keep the
+service implementation in-sync with app config options, and it will make
+impossible to use the same service with older/newer package version,
+which has a slightly different list of available options and will add an
+excessive maintanence load.
+
+However, for some applications with really stable configuration those
+checks can be helpful and should be implemented if possible, for some
+other we can implement them only partially.
+
+The alternative approach applied in some exitsting services is to use
+records for defining the structure of configuration field, it has the
+same downsides of closed-world assumption and a few more problems:
+
+@enumerate
+@item
+It has to replicate all the available options for the app (sometimes
+hundreds or thousands) to allow user express any configuration they
+wants.
+@item
+Having a few records, adds one more layer of abstraction between service
+configuration and resulting app config, including different field
+casing, new semantic units.
+@c provide examples?
+@item
+It harder to implement optional settings, serialization becomes very
+ad-hoc and hard to reuse among other services with the same target
+config format.
+@end enumerate
+
+Exceptions can exist, but the overall idea is to provide a lispy syntax
+for target configuration.  Take a look at sway example configuration
+(which also can be used for i3).  The following value of @code{config}
+field of @code{home-sway-configuration}:
+
+@example
+`((include ,(local-file "./sway/config"))
+  (bindsym $mod+Ctrl+Shift+a exec emacsclient -c --eval "'(eshell)'")
+  (bindsym $mod+Ctrl+Shift+o "[class=\"IceCat\"]" kill)
+  (input * ((xkb_layout us,ru)
+            (xkb_variant dvorak,))))
+@end example
+
+would yield something like:
+
+@example
+include /gnu/store/408jwvh6wxxn1j85lj95fniih05gx5xj-config
+bindsym $mod+Ctrl+Shift+a exec emacsclient -c --eval '(eshell)'
+bindsym $mod+Ctrl+Shift+o [class="IceCat"] kill
+input * @{
+    xkb_layout us,ru
+    xkb_variant dvorak,
+@}
+@end example
+
+The mapping between scheme code and resulting configuration is quite
+obvious.  The serialization code with some type and structure checks
+takes less than 70 lines and every possible sway/i3 configuration can be
+expressed using this field.
+
+@subsubheading Let User Escape
+Sometimes user already have a configuration file for an app, make sure
+that it is possible to reuse it directly without rewriting.  In the
+example above, the following snippet allows to include already existing
+config to the newly generated one utilizing @code{include} directive of
+i3/sway config language:
+
+@example
+(include ,(local-file "./sway/config"))
+@end example
+
+When building a resulting config the file-like objects are substituted
+with a path of the file in the store and sway's @code{include} loads
+this file during startup.  The way file-like objects are treated here
+also allows to specify paths to plugins or other binary files like:
+@code{(load-plugin ,(file-append plugin-package "/share/plugin.so"))}
+(the example value for imaginary service configuration config file
+field).
+
+In some cases target configuration language may not have such
+@code{include} directive and can't provide such a functionallity, to
+workaround it we can do the following trick:
+
+@example
+`(#~(call-with-input-file
+     #$(local-file "./sway/config")
+     (@@ (ice-9 textual-ports) get-string-all)))
+@end example
+
+G-expressions get serialized to its values, and the example above reads
+the content of the file-like object and inserts it in the resulting
+configuration file.
+
+Following these simple rules will help to make a simple, consistent and
+maintainable service configurations, will let user express any possible
+needs and reuse existing configuration files.
+
 @node Service Reference
 @subsection Service Reference
 
-- 
2.34.0