From 28614ed7a1e3ace824c122237bdc0e5e0b62c5c3 Mon Sep 17 00:00:00 2001 From: Robert Helgesson Date: Sun, 4 Jun 2023 10:53:26 +0200 Subject: [PATCH] lib: add functions to create DAGs from lists --- docs/writing-modules.adoc | 98 ++++++++++++++++++++++++++-- modules/lib/dag.nix | 28 +++++++- tests/lib/types/dag-merge-result.txt | 8 +++ tests/lib/types/dag-merge.nix | 22 ++++++- 4 files changed, 149 insertions(+), 7 deletions(-) diff --git a/docs/writing-modules.adoc b/docs/writing-modules.adoc index 7f0e79cd..5e4802ce 100644 --- a/docs/writing-modules.adoc +++ b/docs/writing-modules.adoc @@ -11,12 +11,13 @@ The module system in Home Manager is based entirely on the NixOS module system s Overall the basic option types are the same in Home Manager as NixOS. A few Home Manager options, however, make use of custom types that are worth describing in more detail. These are the option types `dagOf` and `gvariant` that are used, for example, by <> and <>. -`hm.types.dagOf`:: +[[sec-option-types-dag]]`hm.types.dagOf`:: Options of this type have attribute sets as values where each member is a node in a {wikipedia-dag}[directed acyclic graph] (DAG). This allows the attribute set entries to express dependency relations among themselves. This can, for example, be used to control the order of match blocks in a OpenSSH client configuration or the order of activation script blocks in <>. + A number of functions are provided to create DAG nodes. The functions are shown below with examples using an option `foo.bar` of type `hm.types.dagOf types.int`. + -`hm.dag.entryAnywhere (value: T)`::: +-- +[[sec-option-types-dag-entryAnywhere]]`hm.dag.entryAnywhere (value: T) : DagEntry`::: Indicates that `value` can be placed anywhere within the DAG. This is also the default for plain attribute set entries, that is + [source,nix] @@ -37,7 +38,7 @@ foo.bar = { + are equivalent. + -`hm.dag.entryAfter (afters: list string) (value: T)`::: +[[sec-option-types-dag-entryAfter]]`hm.dag.entryAfter (afters: list string) (value: T) : DagEntry` ::: Indicates that `value` must be placed _after_ each of the attribute names in the given list. For example + [source,nix] @@ -50,7 +51,7 @@ foo.bar = { + would place `b` after `a` in the graph. + -`hm.dag.entryBefore (befores: list string) (value: T)`::: +[[sec-option-types-dag-entryBefore]]`hm.dag.entryBefore (befores: list string) (value: T) : DagEntry` ::: Indicates that `value` must be placed _before_ each of the attribute names in the given list. For example + [source,nix] @@ -63,7 +64,7 @@ foo.bar = { + would place `b` before `a` in the graph. + -`hm.dag.entryBetween (befores: list string) (afters: list string) (value: T)`::: +[[sec-option-types-dag-entryBetween]]`hm.dag.entryBetween (befores: list string) (afters: list string) (value: T) : DagEntry` ::: Indicates that `value` must be placed _before_ the attribute names in the first list and _after_ the attribute names in the second list. For example + [source,nix] @@ -76,6 +77,93 @@ foo.bar = { ---- + would place `c` before `b` and after `a` in the graph. +-- ++ +There are also a set of functions that generate a DAG from a list. +These are convenient when you just want to have a linear list of DAG entries, +without having to manually enter the relationship between each entry. +Each of these functions take a `tag` as argument and the DAG entries will be named `${tag}-${index}`. + +[[sec-option-types-dag-entriesAnywhere]]`hm.dag.entriesAnywhere (tag: string) (values: [T]) : Dag`::: +Creates a DAG with the given values with each entry labeled using the given tag. For example ++ +[source,nix] +foo.bar = hm.dag.entriesAnywhere "a" [ 0 1 ]; ++ +is equivalent to ++ +[source,nix] +---- +foo.bar = { + a-0 = 0; + a-1 = hm.dag.entryAfter [ "a-0" ] 1; +} +---- ++ +[[sec-option-types-dag-entriesAfter]]`hm.dag.entriesAfter (tag: string) (afters: list string) (values: [T]) : Dag`::: +Creates a DAG with the given values with each entry labeled using the given tag. +The list of values are placed are placed _after_ each of the attribute names in `afters`. +For example ++ +[source,nix] +foo.bar = + { b = 0; } + // hm.dag.entriesAfter "a" [ "b" ] [ 1 2 ]; ++ +is equivalent to ++ +[source,nix] +---- +foo.bar = { + b = 0; + a-0 = hm.dag.entryAfter [ "b" ] 1; + a-1 = hm.dag.entryAfter [ "a-0" ] 2; +} +---- ++ +[[sec-option-types-dag-entriesBefore]]`hm.dag.entriesBefore (tag: string) (befores: list string) (values: [T]) : Dag`::: +Creates a DAG with the given values with each entry labeled using the given tag. +The list of values are placed _before_ each of the attribute names in `befores`. +For example ++ +[source,nix] +foo.bar = + { b = 0; } + // hm.dag.entriesBefore "a" [ "b" ] [ 1 2 ]; ++ +is equivalent to ++ +[source,nix] +---- +foo.bar = { + b = 0; + a-0 = 1; + a-1 = hm.dag.entryBetween [ "b" ] [ "a-0" ] 2; +} +---- ++ +[[sec-option-types-dag-entriesBetween]]`hm.dag.entriesBetween (tag: string) (befores: list string) (afters: list string) (values: [T]) : Dag`::: +Creates a DAG with the given values with each entry labeled using the given tag. +The list of values are placed _before_ each of the attribute names in `befores` +and _after_ each of the attribute names in `afters`. +For example ++ +[source,nix] +foo.bar = + { b = 0; c = 3; } + // hm.dag.entriesBetween "a" [ "b" ] [ "c" ] [ 1 2 ]; ++ +is equivalent to ++ +[source,nix] +---- +foo.bar = { + b = 0; + c = 3; + a-0 = hm.dag.entryAfter [ "c" ] 1; + a-1 = hm.dag.entryBetween [ "b" ] [ "a-0" ] 2; +} +---- [[sec-option-types-gvariant]]`hm.types.gvariant`:: This type is useful for options representing {gvariant-description}[GVariant] values. The type accepts all primitive GVariant types as well as arrays, tuples, ``maybe'' types, and dictionaries. diff --git a/modules/lib/dag.nix b/modules/lib/dag.nix index 50044a0b..b9e75e8c 100644 --- a/modules/lib/dag.nix +++ b/modules/lib/dag.nix @@ -9,7 +9,7 @@ { lib }: -let inherit (lib) all filterAttrs hm mapAttrs toposort; +let inherit (lib) all filterAttrs head hm mapAttrs length tail toposort; in { empty = { }; @@ -100,4 +100,30 @@ in { entryAfter = hm.dag.entryBetween [ ]; entryBefore = before: hm.dag.entryBetween before [ ]; + + # Given a list of entries, this function places them in order within the DAG. + # Each entry is labeled "${tag}-${entry index}" and other DAG entries can be + # added with 'before' or 'after' referring these indexed entries. + # + # The entries as a whole can be given a relation to other DAG nodes. All + # generated nodes are then placed before or after those dependencies. + entriesBetween = tag: + let + go = i: before: after: entries: + let + name = "${tag}-${toString i}"; + i' = i + 1; + in if entries == [ ] then + hm.dag.empty + else if length entries == 1 then { + "${name}" = hm.dag.entryBetween before after (head entries); + } else + { + "${name}" = hm.dag.entryAfter after (head entries); + } // go (i + 1) before [ name ] (tail entries); + in go 0; + + entriesAnywhere = tag: hm.dag.entriesBetween tag [ ] [ ]; + entriesAfter = tag: hm.dag.entriesBetween tag [ ]; + entriesBefore = tag: before: hm.dag.entriesBetween tag before [ ]; } diff --git a/tests/lib/types/dag-merge-result.txt b/tests/lib/types/dag-merge-result.txt index 0330161e..30df9dec 100644 --- a/tests/lib/types/dag-merge-result.txt +++ b/tests/lib/types/dag-merge-result.txt @@ -2,3 +2,11 @@ before:before merged:left,middle,middle,right between:between after:after +list-anywhere-0:list-anywhere-0 +list-before-0:list-before-0,sneaky-merge +list-before-1:list-before-1 +list-anywhere-1:list-anywhere-1 +inside-list:inside-list +list-after-0:list-after-0 +list-after-1:list-after-1 +list-anywhere-2:list-anywhere-2 diff --git a/tests/lib/types/dag-merge.nix b/tests/lib/types/dag-merge.nix index 5d502924..a77c71ea 100644 --- a/tests/lib/types/dag-merge.nix +++ b/tests/lib/types/dag-merge.nix @@ -27,7 +27,27 @@ in { { merged = dag.entryBefore [ "between" ] "middle"; } { merged = mkBefore "left"; } { merged = dag.entryBetween [ "after" ] [ "before" ] (mkAfter "right"); } - { merged = dag.entryBefore [ "between" ] "middle"; } + { + merged = dag.entryBefore [ "between" ] "middle"; + } + + # Some tests of list entries. + (dag.entriesAnywhere "list-anywhere" [ + "list-anywhere-0" + "list-anywhere-1" + "list-anywhere-2" + ]) + { inside-list = dag.entryAfter [ "list-anywhere-1" ] "inside-list"; } + (dag.entriesBefore "list-before" [ "list-anywhere-1" ] [ + "list-before-0" + "list-before-1" + ]) + (dag.entriesAfter "list-after" [ "list-before-0" ] [ + "list-after-0" + "list-after-1" + ]) + (dag.entriesAnywhere "list-empty" [ ]) + { "list-before-0" = mkAfter "sneaky-merge"; } ]; home.file."result.txt".text = result;