services: configuration: Add environment variable serializer.

This patch implements a general API to serialize configuration records
to list of pairs representing environment variables. The car of each
pair represents the variable name and the cdr the variable value.

* gnu/services/configuration/environment-variables.scm: New file.
(serialize-string-environment-variable)
(serialize-maybe-string-environment-variable)
(serialize-boolean-environment-variable)
(serialize-maybe-boolean-environment-variable)
(serialize-number-environment-variable)
(serialize-maybe-number-environment-variable): New variables.
(serialize-environment-variables): New variable.
* gnu/services/configuration/utils.scm: New file.
(uglify-snake-case): New variable.
* tests/services/configuration.scm: Add tests for environment serializer.
(wrong type for a field): Adjust error location.
* doc/guix.texi: Document it.

Change-Id: I81a166576f94d3c8f5bf78c82a02183689a3091c
Signed-off-by: Liliana Marie Prikler <liliana.prikler@gmail.com>
This commit is contained in:
Giacomo Leidi
2026-03-08 17:27:13 +01:00
committed by Liliana Marie Prikler
parent 2abfd1370f
commit 0b8e838208
4 changed files with 362 additions and 2 deletions

View File

@@ -51663,6 +51663,63 @@ phone-number = 0
is-married = true is-married = true
@end example @end example
@subsubsection Serializing to environment variables
@cindex environment variables, serialization of configuration records
There are services which expect their configuration as environment variables.
The @code{(gnu services configuration environment-variables)} module provides
facilities to serialize configuration records from
@code{(gnu services configuration)} to list of pairs representing environment
variables.
For example this configuration record:
@lisp
(define-configuration/no-serialization server
(ssh-port
(number 22)
"The public SSH port of the server.")
(fqdn
(maybe-string)
"The fully qualified domain name of the server.")
(active?
(boolean #f)
"Whether or not the server should be activated."))
(define my-server
(server
(ssh-port 20022)
(active? #t)))
@end lisp
with this call:
@lisp
(serialize-environment-variables my-server server-fields
#:true-value "1"
#:false-value "0")
@end lisp
would yield:
@lisp
'(("SSH_PORT" . "20022")
("ACTIVE" . "1"))
@end lisp
@anchor{serialize-environment-variables-procedure}
@deffn {Procedure} serialize-environment-variables @var{config} @var{fields} @
[@var{selection} #f] [@var{negate?} #f] [#:prefix #f] @
[#:true-value "true"] [#:false-value "false"]
Serializes the fields whose name is included in SELECTION from CONFIG, a
configuration from @code{(gnu services configuration)}, and FIELDS, the
list of its field records, to a list of pairs. When NEGATE? is #t all services
not included in SELECTION will be serialized. Each pair represents an
environment variable. The first element of each pair is the variable name, the
second is the value. When PREFIX is a string it is prepended to the variable
name. TRUE-VALUE and FALSE-VALUE will be used as a representation for
respectfully @code{#t} and @code{#f}.
@end deffn
@c ********************************************************************* @c *********************************************************************
@cindex troubleshooting, Guix System @cindex troubleshooting, Guix System

View File

@@ -0,0 +1,143 @@
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2026 Giacomo Leidi <goodoldpaul@autistici.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
(define-module (gnu services configuration environment-variables)
#:use-module (gnu services configuration)
#:use-module (gnu services configuration utils)
#:use-module (guix diagnostics)
#:use-module (guix i18n)
#:use-module (ice-9 match)
#:use-module (srfi srfi-1)
#:export (serialize-string-environment-variable
serialize-boolean-environment-variable
serialize-number-environment-variable
serialize-maybe-string-environment-variable
serialize-maybe-boolean-environment-variable
serialize-maybe-number-environment-variable
serialize-environment-variables))
(define* (field-name->environment-variable field-name
#:key prefix
(uglify uglify-snake-case))
"Serializes FIELD-NAME, a field name from @code{(gnu services configuration)},
to an environment variable name through UGLIFY, by default a procedure that is
passed FIELD-NAME, and returns a snake case string representation of
the field name. Trailing @code{?} in the name are dropped and @code{-} get
replaced by @code{_}. When PREFIX is a string, it is prepended to the result.
The result of UGLIFY is then upcased and returned.
For example the procedure would convert @code{'a-field} to @code{\"A_FIELD\"}."
(let ((variable (string-upcase
(uglify field-name))))
(if (string? prefix)
(string-append prefix variable)
variable)))
(define* (serialize-string-environment-variable field-name value
#:key prefix
#:allow-other-keys)
(cons (field-name->environment-variable field-name #:prefix prefix)
value))
(define* (serialize-maybe-string-environment-variable field-name value
#:key prefix
#:allow-other-keys)
(if (maybe-value-set? value)
(serialize-string-environment-variable field-name value #:prefix prefix)
#f))
(define* (serialize-boolean-environment-variable field-name value
#:key prefix
(true-value "true")
(false-value "false")
#:allow-other-keys)
(serialize-string-environment-variable
field-name (if value true-value false-value)
#:prefix prefix))
(define* (serialize-maybe-boolean-environment-variable field-name value
#:key prefix
#:allow-other-keys)
(if (maybe-value-set? value)
(serialize-boolean-environment-variable field-name value #:prefix prefix)
#f))
(define* (serialize-number-environment-variable field-name value
#:key prefix
#:allow-other-keys)
(cons (field-name->environment-variable field-name #:prefix prefix)
(number->string value)))
(define* (serialize-maybe-number-environment-variable field-name value
#:key prefix
#:allow-other-keys)
(if (maybe-value-set? value)
(serialize-number-environment-variable field-name value #:prefix prefix)
#f))
(define (environment-variable-serializer field)
(define type (configuration-field-type field))
(match type
('string serialize-string-environment-variable)
('maybe-string serialize-maybe-string-environment-variable)
('number serialize-number-environment-variable)
('integer serialize-number-environment-variable)
('positive serialize-number-environment-variable)
('maybe-number serialize-maybe-number-environment-variable)
('maybe-integer serialize-maybe-number-environment-variable)
('maybe-positive serialize-maybe-number-environment-variable)
('boolean serialize-boolean-environment-variable)
('maybe-boolean serialize-boolean-environment-variable)
(_
(raise
(formatted-message
(G_ "Unknown environment-variable field type: ~a")
type)))))
(define* (serialize-environment-variables config fields
#:optional selection negate?
#:key prefix
(true-value "true")
(false-value "false"))
"Serializes the fields whose name is included in SELECTION from CONFIG, a
configuration from @code{(gnu services configuration)}, and FIELDS, the
list of its field records, to a list of pairs. When NEGATE? is #t all services
not included in SELECTION will be serialized. Each pair represents an
environment variable. The first element of each pair is the variable name, the
second is the value. When PREFIX is a string it is prepended to the variable
name. TRUE-VALUE and FALSE-VALUE will be used as a representation for
respectfully @code{#t} and @code{#f}."
(define selected-names
(or selection
(map configuration-field-name fields)))
(define filtered
(filter-configuration-fields fields selected-names negate?))
(define getters
(map configuration-field-getter filtered))
(define names
(map configuration-field-name filtered))
(define serializers
(map environment-variable-serializer filtered))
(filter-map (match-lambda ((serializer name getter)
(serializer name (getter config)
#:prefix prefix
#:true-value true-value
#:false-value false-value)))
(zip serializers names getters)))

View File

@@ -0,0 +1,35 @@
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2026 Giacomo Leidi <goodoldpaul@autistici.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
(define-module (gnu services configuration utils)
#:use-module (ice-9 string-fun)
#:export (uglify-snake-case))
(define (uglify-snake-case field-name)
"Serializes FIELD-NAME, a field name from @code{(gnu services configuration)},
to a downcased, snake case string representation of the field name. Trailing
@code{?} in the name are dropped and dashes are replaced with underscores.
For example the procedure would convert @code{'A-Field?} to @code{\"a_field\"}."
(define str (symbol->string field-name))
(string-downcase
(string-replace-substring
(if (string-suffix? "?" str)
(string-drop-right str 1)
str)
"-" "_")))

View File

@@ -3,6 +3,7 @@
;;; Copyright © 2021 Xinglu Chen <public@yoctocell.xyz> ;;; Copyright © 2021 Xinglu Chen <public@yoctocell.xyz>
;;; Copyright © 2022 Ludovic Courtès <ludo@gnu.org> ;;; Copyright © 2022 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2023 Bruno Victal <mirai@makinata.eu> ;;; Copyright © 2023 Bruno Victal <mirai@makinata.eu>
;;; Copyright © 2026 Giacomo Leidi <therewasa@fishinthecalculator.me>
;;; ;;;
;;; This file is part of GNU Guix. ;;; This file is part of GNU Guix.
;;; ;;;
@@ -21,6 +22,7 @@
(define-module (tests services configuration) (define-module (tests services configuration)
#:use-module (gnu services configuration) #:use-module (gnu services configuration)
#:use-module (gnu services configuration environment-variables)
#:use-module (guix diagnostics) #:use-module (guix diagnostics)
#:use-module (guix gexp) #:use-module (guix gexp)
#:autoload (guix i18n) (G_) #:autoload (guix i18n) (G_)
@@ -48,14 +50,14 @@
(port-configuration-port (port-configuration))) (port-configuration-port (port-configuration)))
(test-equal "wrong type for a field" (test-equal "wrong type for a field"
'("configuration.scm" 59 11) ;error location '("configuration.scm" 61 11) ;error location
(guard (c ((configuration-error? c) (guard (c ((configuration-error? c)
(let ((loc (error-location c))) (let ((loc (error-location c)))
(list (basename (location-file loc)) (list (basename (location-file loc))
(location-line loc) (location-line loc)
(location-column loc))))) (location-column loc)))))
(port-configuration (port-configuration
;; This is line 58; the test relies on line/column numbers! ;; This is line 60; the test relies on line/column numbers!
(port "This is not a number!")))) (port "This is not a number!"))))
(define-configuration port-configuration-cs (define-configuration port-configuration-cs
@@ -363,3 +365,126 @@
(config-with-maybe-string/no-serialization-name (config-with-maybe-string/no-serialization-name
(config-with-maybe-string/no-serialization (config-with-maybe-string/no-serialization
(name "foo"))))) (name "foo")))))
;;;
;;; environment-variables serializer
;;;
(define-configuration/no-serialization env-config
(port (number 80) "")
(count maybe-number "")
(name string "")
(url maybe-string "")
(active? (boolean #f) ""))
(test-group "environment variables serializer"
(test-equal "basic serialization"
'(("PORT" . "70")
("COUNT" . "80")
("NAME" . "Hello World")
("URL" . "https://example.org")
("ACTIVE" . "true"))
(serialize-environment-variables
(env-config
(port 70)
(name "Hello World")
(url "https://example.org")
(active? #t)
(count 80))
env-config-fields))
(test-equal "field selection"
'(("PORT" . "80"))
(serialize-environment-variables
(env-config
(name "Hello World"))
env-config-fields
'(port)))
(test-equal "field negative selection"
'(("NAME" . "Hello World")
("ACTIVE" . "false"))
(serialize-environment-variables
(env-config
(name "Hello World"))
env-config-fields
'(port)
#t))
(test-equal "variable prefixes"
'(("TEST_PORT" . "80")
("TEST_NAME" . "Hello World")
("TEST_ACTIVE" . "false"))
(serialize-environment-variables
(env-config
(name "Hello World"))
env-config-fields
#:prefix "TEST_"))
(test-equal "boolean serialization"
'(("PORT" . "80")
("NAME" . "Hello World")
("ACTIVE" . "1"))
(serialize-environment-variables
(env-config
(name "Hello World")
(active? #t))
env-config-fields
#:true-value "1"
#:false-value "0"))
(test-equal "full record serialization"
'(("TEST_COUNT" . "800")
("TEST_NAME" . "Hello World")
("TEST_URL" . "https://example.org")
("TEST_ACTIVE" . "1"))
(serialize-environment-variables
(env-config
(port 90)
(name "Hello World")
(count 800)
(url "https://example.org")
(active? #t))
env-config-fields
'(port)
#t
#:prefix "TEST_"
#:true-value "1"
#:false-value "0")))
(define-configuration another-env-config
(port
(number 80)
""
(serializer serialize-number-environment-variable))
(count
(maybe-number)
""
(serializer serialize-maybe-number-environment-variable))
(name
(string)
""
(serializer serialize-string-environment-variable))
(url
(maybe-string)
""
(serializer serialize-maybe-string-environment-variable))
(active?
(boolean #f)
""
(serializer serialize-boolean-environment-variable)))
(test-group "environment variables serializer, with field serializers"
(test-assert "full record serialization"
(gexp?
(serialize-configuration
(another-env-config
(port 90)
(name "Hello World")
(count 800)
(url "https://example.org")
(active? #t))
another-env-config-fields))))