diff --git a/crates/mq-lang/modules/module_tests.mq b/crates/mq-lang/modules/module_tests.mq index a02b4c2e7..2b11b586e 100644 --- a/crates/mq-lang/modules/module_tests.mq +++ b/crates/mq-lang/modules/module_tests.mq @@ -128,8 +128,9 @@ def test_toml_parse(): end def test_toml_to_json(): - let result = toml::toml_to_json(toml::toml_parse(toml_input)) - | assert_eq(result, "{\"dependencies\":{},\"package\":{\"name\":\"test-package\",\"authors\":[\"Test Author \"],\"description\":\"A test TOML configuration file\",\"edition\":\"2021\",\"license\":\"MIT\",\"version\":\"1.0.0\"}}") + let parsed = toml::toml_parse(toml_input) + | let result = toml::toml_to_json(parsed) + | assert_eq(json::json_parse(result), parsed) end # -- YAML Tests -- @@ -731,6 +732,48 @@ def test_semver_min(): | assert_eq(semver::semver_to_string(min), "1.0.0") end +def test_semver_satisfies_range_true(): + assert_true(semver::semver_satisfies("1.5.0", ">=1.0.0,<2.0.0")) +end + +def test_semver_satisfies_range_false(): + assert_false(semver::semver_satisfies("2.0.0", ">=1.0.0,<2.0.0")) +end + +def test_semver_satisfies_range_lower_bound_inclusive(): + assert_true(semver::semver_satisfies("1.0.0", ">=1.0.0,<2.0.0")) +end + +def test_semver_satisfies_exact_match(): + assert_true(semver::semver_satisfies("1.2.3", "1.2.3")) + | assert_false(semver::semver_satisfies("1.2.4", "1.2.3")) +end + +def test_semver_satisfies_eq_operator(): + assert_true(semver::semver_satisfies("1.2.3", "=1.2.3")) + | assert_true(semver::semver_satisfies("1.2.3", "==1.2.3")) +end + +def test_semver_satisfies_not_equal(): + assert_true(semver::semver_satisfies("1.2.4", "!=1.2.3")) + | assert_false(semver::semver_satisfies("1.2.3", "!=1.2.3")) +end + +def test_semver_satisfies_gt_lt(): + assert_true(semver::semver_satisfies("1.5.0", ">1.0.0")) + | assert_false(semver::semver_satisfies("1.0.0", ">1.0.0")) + | assert_true(semver::semver_satisfies("1.0.0", "<2.0.0")) + | assert_false(semver::semver_satisfies("2.0.0", "<2.0.0")) +end + +def test_semver_satisfies_ignores_whitespace(): + assert_true(semver::semver_satisfies("1.5.0", " >= 1.0.0 , < 2.0.0 ")) +end + +def test_semver_satisfies_with_prerelease(): + assert_true(semver::semver_satisfies("1.0.0-alpha", "<1.0.0")) +end + # -- Markdown Builder (md) Tests -- def test_md_h(): diff --git a/crates/mq-lang/modules/semver.mq b/crates/mq-lang/modules/semver.mq index cbbbacaa6..422c92d2c 100644 --- a/crates/mq-lang/modules/semver.mq +++ b/crates/mq-lang/modules/semver.mq @@ -173,3 +173,45 @@ end def semver_min(versions): fold(versions[1:len(versions)], versions[0], fn(acc, v): if (semver_lt(v, acc)): v else: acc;) end + +# Parses a single version comparator (e.g. ">=1.0.0") into a dict with op and parsed version fields. +def _semver_parse_comparator(comp): + let c = trim(comp) + | if (starts_with(c, ">=")): + {op: ">=", version: semver_parse(trim(ltrimstr(c, ">=")))} + elif (starts_with(c, "<=")): + {op: "<=", version: semver_parse(trim(ltrimstr(c, "<=")))} + elif (starts_with(c, "==")): + {op: "=", version: semver_parse(trim(ltrimstr(c, "==")))} + elif (starts_with(c, "!=")): + {op: "!=", version: semver_parse(trim(ltrimstr(c, "!=")))} + elif (starts_with(c, ">")): + {op: ">", version: semver_parse(trim(ltrimstr(c, ">")))} + elif (starts_with(c, "<")): + {op: "<", version: semver_parse(trim(ltrimstr(c, "<")))} + elif (starts_with(c, "=")): + {op: "=", version: semver_parse(trim(ltrimstr(c, "=")))} + else: + {op: "=", version: semver_parse(c)} +end + +# Checks whether a parsed SemVer dict satisfies a single comparator dict produced by `_semver_parse_comparator`. +def _semver_satisfies_comparator(v, cmp): + let op = cmp[:op] + | let cv = cmp[:version] + | if (op == ">="): semver_gte(v, cv) + elif (op == "<="): semver_lte(v, cv) + elif (op == ">"): semver_gt(v, cv) + elif (op == "<"): semver_lt(v, cv) + elif (op == "!="): !semver_eq(v, cv) + else: semver_eq(v, cv) +end + +# Returns true if the given version string satisfies every comma-separated comparator in `range`. +# Supported comparators: "=", "==", "!=", ">", ">=", "<", "<=". A bare version (no operator) requires an exact match. +# Example: semver_satisfies("1.5.0", ">=1.0.0,<2.0.0") == true +def semver_satisfies(version, range): + let v = semver_parse(version) + | let comparators = map(split(range, ","), _semver_parse_comparator) + | all(comparators, fn(cmp): _semver_satisfies_comparator(v, cmp);) +end