Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] lib.sources.predicateFilter: init #221361

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions lib/path/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ let
isPath
split
match
typeOf
;

inherit (lib.lists)
Expand All @@ -18,6 +19,8 @@ let
all
concatMap
foldl'
take
drop
;

inherit (lib.strings)
Expand Down Expand Up @@ -100,8 +103,48 @@ let
# An empty string is not a valid relative path, so we need to return a `.` when we have no components
(if components == [] then "." else concatStringsSep "/" components);

# Deconstruct a path value type into:
# - root: The filesystem root of the path, generally `/`
# - components: All the path's components
#
# This is similar to `splitString "/" (toString path)` but safer
# because it can distinguish different filesystem roots
deconstructPath =
let
recurse = components: path:
# If the parent of a path is the path itself, then it's a filesystem root
if path == dirOf path then { root = path; inherit components; }
else recurse ([ (baseNameOf path) ] ++ components) (dirOf path);
in recurse [];

# Same as lib.path.hasPrefix (see docs), but it can be reused by lib.path.hasProperPrefix as well
internalHasPrefix = function: prefix: path:
let
deconPrefix = deconstructPath prefix;
deconPath = deconstructPath path;
in
assert assertMsg
(isPath prefix)
"${function}: First argument is of type ${typeOf prefix}, but a path was expected";
assert assertMsg
(isPath path)
"${function}: Second argument is of type ${typeOf path}, but a path was expected";
assert assertMsg
(deconPrefix.root == deconPath.root) ''
${function}: Filesystem roots must be the same for both paths, but paths with different roots were given:
first argument: "${toString prefix}" (root "${toString deconPrefix.root}")
second argument: "${toString path}" (root "${toString deconPath.root}")'';
take (length deconPrefix.components) deconPath.components == deconPrefix.components;

in /* No rec! Add dependencies on this file at the top. */ {

deconstructPath = path:
let deconstructed = deconstructPath path;
in {
root = deconstructed.root;
subpath = joinRelPath deconstructed.components;
};

/* Append a subpath string to a path.

Like `path + ("/" + string)` but safer, because it errors instead of returning potentially surprising results.
Expand Down Expand Up @@ -149,6 +192,74 @@ in /* No rec! Add dependencies on this file at the top. */ {
${subpathInvalidReason subpath}'';
path + ("/" + subpath);

/*
Whether the second path is a prefix of the first path, or equal to it.
Throws an error if the paths don't share the same filesystem root.

Laws:

- Equivalent to whether some subpath exists that can be appended to the first path to get the second path:

hasPrefix p q <-> exists s . append p s == q

- `hasPrefix` is a [non-strict partial order](https://en.wikipedia.org/wiki/Partially_ordered_set#Non-strict_partial_order) over the set of all path values

- `lib.path.hasProperPrefix` is the [corresponding strict partial order](https://en.wikipedia.org/wiki/Partially_ordered_set#Correspondence_of_strict_and_non-strict_partial_order_relations):

hasPrefix p q <-> hasProperPrefix p q || p == q

Type:
hasPrefix :: Path -> Path -> Bool

Example:
hasPrefix /foo /foo/bar
=> true
hasPrefix /foo /foo
=> true
hasPrefix /foo/bar /foo
=> false
hasPrefix /. /foo
=> true
*/
hasPrefix =
# The potential path prefix
prefix:
# The path that that might start with the given prefix
path:
internalHasPrefix "lib.path.hasPrefix" prefix path;

/*
Whether the second path is a prefix of the first path and not equal to it.
Throws an error if the paths don't share the same filesystem root.

Laws:

- `hasProperPrefix` is a [strict partial order](https://en.wikipedia.org/wiki/Partially_ordered_set#Strict_partial_order) over the set of all path values

- `lib.path.hasPrefix` is the [corresponding non-strict partial order](https://en.wikipedia.org/wiki/Partially_ordered_set#Correspondence_of_strict_and_non-strict_partial_order_relations):

hasProperPrefix p q <-> hasPrefix p q && p != q

Type:
hasProperPrefix :: Path -> Path -> Bool

Example:
hasProperPrefix /foo /foo/bar
=> true
hasProperPrefix /foo /foo
=> false
hasProperPrefix /foo/bar /foo
=> false
hasProperPrefix /. /foo
=> true
*/
hasProperPrefix =
# The potential path prefix
prefix:
# The path that that might start with the given prefix
path:
internalHasPrefix "lib.path.hasProperPrefix" prefix path && prefix != path;

/* Whether a value is a valid subpath string.

- The value is a string
Expand Down Expand Up @@ -348,4 +459,40 @@ in /* No rec! Add dependencies on this file at the top. */ {
${subpathInvalidReason subpath}'';
joinRelPath (splitRelPath subpath);

subpath.hasPrefix =
prefix:
assert assertMsg (isValid prefix) ''
lib.path.subpath.hasPrefix: The first argument is not a valid subpath string:
${subpathInvalidReason prefix}'';
let
splitPrefix = splitRelPath prefix;
prefixLength = length splitPrefix;
in
subpath:
assert assertMsg (isValid subpath) ''
lib.path.subpath.hasPrefix: The second argument is not a valid subpath string:
${subpathInvalidReason subpath}'';
take prefixLength (splitRelPath subpath) == splitPrefix;

subpath.removePrefix =
prefix:
assert assertMsg (isValid prefix) ''
lib.path.subpath.removePrefix: The first argument is not a valid subpath string:
${subpathInvalidReason prefix}'';
let
splitPrefix = splitRelPath prefix;
prefixLength = length splitPrefix;
in
subpath:
assert assertMsg (isValid subpath) ''
lib.path.subpath.removePrefix: The second argument is not a valid subpath string:
${subpathInvalidReason subpath}'';
let
splitSubpath = splitRelPath subpath;
in
if take prefixLength splitSubpath == splitPrefix
then joinRelPath (drop prefixLength splitSubpath)
else throw ''
lib.path.subpath.removePrefix: The first argument (${prefix}) is not a subpath prefix of the second argument (${subpath})'';

}
36 changes: 35 additions & 1 deletion lib/path/tests/unit.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{ libpath }:
let
lib = import libpath;
inherit (lib.path) append subpath;
inherit (lib.path) hasPrefix hasProperPrefix append subpath;

cases = lib.runTests {
# Test examples from the lib.path.append documentation
Expand Down Expand Up @@ -40,6 +40,40 @@ let
expected = false;
};

testHasPrefixExample1 = {
expr = hasPrefix /foo /foo/bar;
expected = true;
};
testHasPrefixExample2 = {
expr = hasPrefix /foo /foo;
expected = true;
};
testHasPrefixExample3 = {
expr = hasPrefix /foo/bar /foo;
expected = false;
};
testHasPrefixExample4 = {
expr = hasPrefix /. /foo;
expected = true;
};

testHasProperPrefixExample1 = {
expr = hasProperPrefix /foo /foo/bar;
expected = true;
};
testHasProperPrefixExample2 = {
expr = hasProperPrefix /foo /foo;
expected = false;
};
testHasProperPrefixExample3 = {
expr = hasProperPrefix /foo/bar /foo;
expected = false;
};
testHasProperPrefixExample4 = {
expr = hasProperPrefix /. /foo;
expected = true;
};

# Test examples from the lib.path.subpath.isValid documentation
testSubpathIsValidExample1 = {
expr = subpath.isValid null;
Expand Down
40 changes: 40 additions & 0 deletions lib/sources.nix
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,42 @@ let
outPath = builtins.path { inherit filter name; path = origSrc; };
};

predicateFilterPath = { src, predicate }:
let
orig = toSourceAttributes src;
removeBase = lib.path.subpath.removePrefix (lib.path.deconstructPath orig.origSrc).subpath;
toPath = str: lib.path.append orig.origSrc (removeBase (lib.substring 1 (-1) str));
in fromSourceAttributes {
inherit (orig) name origSrc;
filter = pathString: type: predicate { path = toPath pathString; inherit type; }
&& orig.filter pathString type;
};

predicateFilterSubpath = { src, predicate }:
let
orig = toSourceAttributes src;
removeBase = lib.path.subpath.removePrefix (lib.path.deconstructPath orig.origSrc).subpath;
toSubpath = str: removeBase (lib.substring 1 (-1) str);
in fromSourceAttributes {
inherit (orig) name origSrc;
filter = pathString: type: predicate { subpath = toSubpath pathString; inherit type; }
&& orig.filter pathString type;
};

unionPath = root: list:
lib.sources.predicateFilterPath {
src = root;
predicate = { path, ... }:
lib.any (el: lib.path.hasPrefix path el || lib.path.hasPrefix el path) list;
};

unionSubpath = root: list:
lib.sources.predicateFilterSubpath {
src = root;
predicate = { subpath, ... }:
lib.any (el: lib.path.subpath.hasPrefix subpath el || lib.path.subpath.hasPrefix el subpath) list;
};

in {
inherit
pathType
Expand All @@ -289,5 +325,9 @@ in {
sourceFilesBySuffices

trace
predicateFilterPath
predicateFilterSubpath
unionPath
unionSubpath
;
}