Unverified Commit b611afeb authored by Silvan Mosberger's avatar Silvan Mosberger Committed by GitHub
Browse files

Merge pull request #209099 from infinisil/lib.path.subpath.join

lib.path.subpath.join: init
parents a8e4f58d 1a2c2846
Loading
Loading
Loading
Loading
+77 −0
Original line number Diff line number Diff line
@@ -15,6 +15,9 @@ let
    last
    genList
    elemAt
    all
    concatMap
    foldl'
    ;

  inherit (lib.strings)
@@ -190,6 +193,80 @@ in /* No rec! Add dependencies on this file at the top. */ {
    subpathInvalidReason value == null;


  /* Join subpath strings together using `/`, returning a normalised subpath string.

    Like `concatStringsSep "/"` but safer, specifically:

    - All elements must be valid subpath strings, see `lib.path.subpath.isValid`

    - The result gets normalised, see `lib.path.subpath.normalise`

    - The edge case of an empty list gets properly handled by returning the neutral subpath `"./."`

    Laws:

    - Associativity:

          subpath.join [ x (subpath.join [ y z ]) ] == subpath.join [ (subpath.join [ x y ]) z ]

    - Identity - `"./."` is the neutral element for normalised paths:

          subpath.join [ ] == "./."
          subpath.join [ (subpath.normalise p) "./." ] == subpath.normalise p
          subpath.join [ "./." (subpath.normalise p) ] == subpath.normalise p

    - Normalisation - the result is normalised according to `lib.path.subpath.normalise`:

          subpath.join ps == subpath.normalise (subpath.join ps)

    - For non-empty lists, the implementation is equivalent to normalising the result of `concatStringsSep "/"`.
      Note that the above laws can be derived from this one.

          ps != [] -> subpath.join ps == subpath.normalise (concatStringsSep "/" ps)

    Type:
      subpath.join :: [ String ] -> String

    Example:
      subpath.join [ "foo" "bar/baz" ]
      => "./foo/bar/baz"

      # normalise the result
      subpath.join [ "./foo" "." "bar//./baz/" ]
      => "./foo/bar/baz"

      # passing an empty list results in the current directory
      subpath.join [ ]
      => "./."

      # elements must be valid subpath strings
      subpath.join [ /foo ]
      => <error>
      subpath.join [ "" ]
      => <error>
      subpath.join [ "/foo" ]
      => <error>
      subpath.join [ "../foo" ]
      => <error>
  */
  subpath.join =
    # The list of subpaths to join together
    subpaths:
    # Fast in case all paths are valid
    if all isValid subpaths
    then joinRelPath (concatMap splitRelPath subpaths)
    else
      # Otherwise we take our time to gather more info for a better error message
      # Strictly go through each path, throwing on the first invalid one
      # Tracks the list index in the fold accumulator
      foldl' (i: path:
        if isValid path
        then i + 1
        else throw ''
          lib.path.subpath.join: Element at index ${toString i} is not a valid subpath string:
              ${subpathInvalidReason path}''
      ) 0 subpaths;

  /* Normalise a subpath. Throw an error if the subpath isn't valid, see
  `lib.path.subpath.isValid`

+30 −0
Original line number Diff line number Diff line
@@ -107,6 +107,36 @@ let
      expected = true;
    };

    # Test examples from the lib.path.subpath.join documentation
    testSubpathJoinExample1 = {
      expr = subpath.join [ "foo" "bar/baz" ];
      expected = "./foo/bar/baz";
    };
    testSubpathJoinExample2 = {
      expr = subpath.join [ "./foo" "." "bar//./baz/" ];
      expected = "./foo/bar/baz";
    };
    testSubpathJoinExample3 = {
      expr = subpath.join [ ];
      expected = "./.";
    };
    testSubpathJoinExample4 = {
      expr = (builtins.tryEval (subpath.join [ /foo ])).success;
      expected = false;
    };
    testSubpathJoinExample5 = {
      expr = (builtins.tryEval (subpath.join [ "" ])).success;
      expected = false;
    };
    testSubpathJoinExample6 = {
      expr = (builtins.tryEval (subpath.join [ "/foo" ])).success;
      expected = false;
    };
    testSubpathJoinExample7 = {
      expr = (builtins.tryEval (subpath.join [ "../foo" ])).success;
      expected = false;
    };

    # Test examples from the lib.path.subpath.normalise documentation
    testSubpathNormaliseExample1 = {
      expr = subpath.normalise "foo//bar";