Unverified Commit 644527dd authored by Robert Hensing's avatar Robert Hensing Committed by Johannes Kirschbauer
Browse files

lib.modules: init types checkAndMerge to allow adding 'valueMeta' attributes

This allows individual types to add attributes that would be discarded during normal evaluation.
Some examples:

types.submodule performs a submodule evluation which yields an 'evalModules' result.
It returns '.config' but makes the original result accessible via 'valueMeta' allowing introspection of '.options' and all other kinds of module evaluation results

types.attrsOf returns an attribute set of the nestedType.
It makes each valueMeta available under the corresponding attribute name.
parent bcf94dd3
Loading
Loading
Loading
Loading
+13 −1
Original line number Diff line number Diff line
@@ -1121,6 +1121,7 @@ let
      files = map (def: def.file) res.defsFinal;
      definitionsWithLocations = res.defsFinal;
      inherit (res) isDefined;
      inherit (res.checkedAndMerged) valueMeta;
      # This allows options to be correctly displayed using `${options.path.to.it}`
      __toString = _: showOption loc;
    };
@@ -1164,7 +1165,9 @@ let
    # Type-check the remaining definitions, and merge them. Or throw if no definitions.
    mergedValue =
      if isDefined then
        if all (def: type.check def.value) defsFinal then
        if type.checkAndMerge or null != null then
          checkedAndMerged.value
        else if all (def: type.check def.value) defsFinal then
          type.merge loc defsFinal
        else
          let
@@ -1177,6 +1180,15 @@ let
        throw
          "The option `${showOption loc}' was accessed but has no value defined. Try setting the option.";

    checkedAndMerged =
      if type.checkAndMerge or null != null then
        type.checkAndMerge loc defsFinal
      else
        {
          value = mergedValue;
          valueMeta = { };
        };

    isDefined = defsFinal != [ ];

    optionalValue = if isDefined then { value = mergedValue; } else { };
+85 −44
Original line number Diff line number Diff line
@@ -48,6 +48,7 @@ let
    mergeOneOption
    mergeUniqueOption
    showFiles
    showDefs
    showOption
    ;
  inherit (lib.strings)
@@ -204,6 +205,10 @@ let
        # definition values and locations (e.g. [ { file = "/foo.nix";
        # value = 1; } { file = "/bar.nix"; value = 2 } ]).
        merge ? mergeDefaultOption,
        #
        # This field does not have a default implementation, so that users' changes
        # to `check` and `merge` are propagated.
        checkAndMerge ? null,
        # Whether this type has a value representing nothingness. If it does,
        # this should be a value of the form { value = <the nothing value>; }
        # If it doesn't, this should be {}
@@ -252,6 +257,7 @@ let
          deprecationMessage
          nestedTypes
          descriptionClass
          checkAndMerge
          ;
        functor =
          if functor ? wrappedDeprecationMessage then
@@ -705,10 +711,11 @@ let
          }";
          descriptionClass = "composite";
          check = isList;
          merge =
          merge = loc: defs: (checkAndMerge loc defs).value;
          checkAndMerge =
            loc: defs:
            map (x: x.value) (
              filter (x: x ? value) (
            let
              evals = filter (x: x.optionalValue ? value) (
                concatLists (
                  imap1 (
                    n: def:
@@ -719,12 +726,16 @@ let
                          inherit (def) file;
                          value = def';
                        }
                      ]).optionalValue
                      ])
                    ) def.value
                  ) defs
                )
              )
              );
            in
            {
              value = map (x: x.optionalValue.value or x.mergedValue) evals;
              valueMeta.list = map (v: v.checkedAndMerged.valueMeta) evals;
            };
          emptyValue = {
            value = [ ];
          };
@@ -740,14 +751,16 @@ let
      nonEmptyListOf =
        elemType:
        let
          list = addCheck (types.listOf elemType) (l: l != [ ]);
          list = types.listOf elemType;
        in
        addCheck (
          list
          // {
            description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}";
            emptyValue = { }; # no .value attr, meaning unset
            substSubModules = m: nonEmptyListOf (elemType.substSubModules m);
        };
          }
        ) (l: l != [ ]);

      attrsOf = elemType: attrsWith { inherit elemType; };

@@ -801,42 +814,38 @@ let
          lazy ? false,
          placeholder ? "name",
        }:
        mkOptionType {
        mkOptionType rec {
          name = if lazy then "lazyAttrsOf" else "attrsOf";
          description =
            (if lazy then "lazy attribute set" else "attribute set")
            + " of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
          descriptionClass = "composite";
          check = isAttrs;
          merge =
            if lazy then
              (
                # Lazy merge Function
          merge = loc: defs: (checkAndMerge loc defs).value;
          checkAndMerge =
            loc: defs:
                zipAttrsWith
                  (
                    name: defs:
            let
                      merged = mergeDefinitions (loc ++ [ name ]) elemType defs;
                      # mergedValue will trigger an appropriate error when accessed
                    in
                    merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue
                  )
                  # Push down position info.
                  (pushPositions defs)
              )
              evals =
                if lazy then
                  zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs)
                else
              (
                # Non-lazy merge Function
                loc: defs:
                mapAttrs (n: v: v.value) (
                  filterAttrs (n: v: v ? value) (
                    zipAttrsWith (name: defs: (mergeDefinitions (loc ++ [ name ]) elemType (defs)).optionalValue)
                      # Push down position info.
                      (pushPositions defs)
                  )
                )
                  # Filtering makes the merge function more strict
                  # Meaning it is less lazy
                  filterAttrs (n: v: v.optionalValue ? value) (
                    zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs)
                  );
            in
            {
              value = mapAttrs (
                n: v:
                if lazy then
                  v.optionalValue.value or elemType.emptyValue.value or v.mergedValue
                else
                  v.optionalValue.value
              ) evals;
              valueMeta.attrs = mapAttrs (n: v: v.checkedAndMerged.valueMeta) evals;
            };

          emptyValue = {
            value = { };
          };
@@ -1236,6 +1245,18 @@ let
              modules = [ { _module.args.name = last loc; } ] ++ allModules defs;
              prefix = loc;
            }).config;
          checkAndMerge =
            loc: defs:
            let
              configuration = base.extendModules {
                modules = [ { _module.args.name = last loc; } ] ++ allModules defs;
                prefix = loc;
              };
            in
            {
              value = configuration.config;
              valueMeta = configuration;
            };
          emptyValue = {
            value = { };
          };
@@ -1451,6 +1472,7 @@ let
          nestedTypes.coercedType = coercedType;
          nestedTypes.finalType = finalType;
        };

      /**
        Augment the given type with an additional type check function.

@@ -1459,7 +1481,26 @@ let
        Fixing is not trivial, we appreciate any help!
        :::
      */
      addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; };
      addCheck =
        elemType: check:
        elemType
        // {
          check = x: elemType.check x && check x;
        }
        // (lib.optionalAttrs (elemType.checkAndMerge != null) {
          checkAndMerge =
            loc: defs:
            let
              v = (elemType.checkAndMerge loc defs);
            in
            if all (def: elemType.check def.value && check def.value) defs then
              v
            else
              let
                allInvalid = filter (def: !elemType.check def.value || !check def.value) defs;
              in
              throw "A definition for option `${showOption loc}' is not of type `${elemType.description}'. Definition values:${showDefs allInvalid}";
        });

    };