Repository: casey/just (rust)

Issue:
Add `semver_lt`/`semver_gt`/`semver_eq`/... functions
Thanks for the awesome tool -- `just` has been a huge part of my daily workflow and I take it to "just" about every project I start now. 

One thing I've often run into is the need to parse & check versions of dependencies cross-language/toolchain against each other, and checking/ensuring a certain version of semver often comes up. 

For exact matches this is relatively simple, but it might be nice to do be able to do this a bit more semantically as a utility within `just`.

A somewhat contrived example of what I'd like to be able to do is:

```just
dep := env_var_or_default("DEP", "dep")
dep_version := `dep --version` # let's pretend that this prints the semver version
dep_version_supported := if semver_gte(dep_version, "0.2.0") { "supported" } else { "not supported" }

check:
  echo "your dep version of dep ({{dep_version}}) is {{dep_version_supported}}"
```
Does this seem like something that makes sense as a part of `just`?

Pull request description:
feat: add `semver_matches` utility function
While coordinating different programs using `just`, it's often necessary to compare the *versions* of different binaries or utilitiies. Assuming that the utility in question providers a `--version` switch and can reasonably print out a version, being able to match semver of the utility to an expectation is very useful.

This commit adds a `semver_matches` utility which utilizes the `semver` crate (https://crates.io/crates/semver) to provide matching functionality to `Justfile`s.

Resolves #1712 

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 4aa30724..49c5fa28 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -337,6 +337,7 @@ dependencies = [
  "num_cpus",
  "pretty_assertions",
  "regex",
+ "semver",
  "serde",
  "serde_json",
  "sha2",
@@ -573,6 +574,12 @@ version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
 
+[[package]]
+name = "semver"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
+
 [[package]]
 name = "serde"
 version = "1.0.188"
diff --git a/Cargo.toml b/Cargo.toml
index 20ba6d92..34217b76 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -33,6 +33,7 @@ libc = "0.2.0"
 log = "0.4.4"
 num_cpus = "1.15.0"
 regex = "1.5.4"
+semver = "1.0.20"
 serde = { version = "1.0.130", features = ["derive", "rc"] }
 serde_json = "1.0.68"
 sha2 = "0.10"
diff --git a/README.md b/README.md
index 2d625f09..36bbd242 100644
--- a/README.md
+++ b/README.md
@@ -1241,6 +1241,10 @@ These functions can fail, for example if a path does not have an extension, whic
 - `sha256_file(path)` - Return the SHA-256 hash of the file at `path` as a hexadecimal string.
 - `uuid()` - Return a randomly generated UUID.
 
+#### Semantic Versions
+
+- `semver_matches(version, requirement)`<sup>master</sup> - Check whether a [semantic `version`](https://semver.org), e.g., `"0.1.0"` matches a `requirement`, e.g., `">=0.1.0"`, returning `"true"` if so and `"false"` otherwise.
+
 ### Recipe Attributes
 
 Recipes may be annotated with attributes that change their behavior.
diff --git a/src/function.rs b/src/function.rs
index 84c21c03..086ede1b 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -4,6 +4,7 @@ use {
     ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase,
     ToUpperCamelCase,
   },
+  semver::{Version, VersionReq},
   Function::*,
 };
 
@@ -46,6 +47,7 @@ pub(crate) fn get(name: &str) -> Option<Function> {
     "quote" => Unary(quote),
     "replace" => Ternary(replace),
     "replace_regex" => Ternary(replace_regex),
+    "semver_matches" => Binary(semver_matches),
     "sha256" => Unary(sha256),
     "sha256_file" => Unary(sha256_file),
     "shoutykebabcase" => Unary(shoutykebabcase),
@@ -411,3 +413,23 @@ fn without_extension(_context: &FunctionContext, path: &str) -> Result<String, S
 
   Ok(parent.join(file_stem).to_string())
 }
+
+/// Check whether a string processes properly as semver (e.x. "0.1.0")
+/// and matches a given semver requirement (e.x. ">=0.1.0")
+fn semver_matches(
+  _context: &FunctionContext,
+  version: &str,
+  requirement: &str,
+) -> Result<String, String> {
+  Ok(
+    requirement
+      .parse::<VersionReq>()
+      .map_err(|err| format!("invalid semver requirement: {err}"))?
+      .matches(
+        &version
+          .parse::<Version>()
+          .map_err(|err| format!("invalid semver version: {err}"))?,
+      )
+      .to_string(),
+  )
+}


Test patch:
diff --git a/tests/functions.rs b/tests/functions.rs
index 5511329a..150f293d 100644
--- a/tests/functions.rs
+++ b/tests/functions.rs
@@ -412,6 +412,21 @@ test! {
     stderr: "echo Bar\n",
 }
 
+#[test]
+fn semver_matches() {
+  Test::new()
+    .justfile(
+      "
+      foo:
+        echo {{ semver_matches('0.1.0', '>=0.1.0') }}
+        echo {{ semver_matches('0.1.0', '=0.0.1') }}
+    ",
+    )
+    .stdout("true\nfalse\n")
+    .stderr("echo true\necho false\n")
+    .run();
+}
+
 fn assert_eval_eq(expression: &str, result: &str) {
   Test::new()
     .justfile(format!("x := {expression}"))
diff --git a/tests/test.rs b/tests/test.rs
index e8dc786b..4aad92b7 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -39,6 +39,7 @@ pub(crate) struct Output {
   pub(crate) tempdir: TempDir,
 }
 
+#[must_use]
 pub(crate) struct Test {
   pub(crate) args: Vec<String>,
   pub(crate) current_dir: PathBuf,