Browse Source

Implement all of semver

default_compile_flags
vector-of-bool 5 years ago
parent
commit
a56a3947e0
11 changed files with 620 additions and 25 deletions
  1. +27
    -0
      src/semver/build_metadata.hpp
  2. +114
    -0
      src/semver/ident.cpp
  3. +62
    -0
      src/semver/ident.hpp
  4. +89
    -0
      src/semver/ident.test.cpp
  5. +11
    -0
      src/semver/order.hpp
  6. +40
    -0
      src/semver/prerelease.cpp
  7. +46
    -0
      src/semver/prerelease.hpp
  8. +48
    -0
      src/semver/prerelease.test.cpp
  9. +71
    -9
      src/semver/version.cpp
  10. +34
    -16
      src/semver/version.hpp
  11. +78
    -0
      src/semver/version.test.cpp

+ 27
- 0
src/semver/build_metadata.hpp View File

@@ -0,0 +1,27 @@
#pragma once

#include <semver/ident.hpp>

namespace semver {

class build_metadata {
std::vector<ident> _ids;

public:
build_metadata() = default;

[[nodiscard]] bool empty() const noexcept { return _ids.empty(); }

void add_ident(ident id) noexcept { _ids.push_back(id); }
void add_ident(std::string_view s) { add_ident(ident(s)); }

auto& idents() const noexcept { return _ids; }

static build_metadata parse(std::string_view s) {
build_metadata ret;
ret._ids = ident::parse_dotted_seq(s);
return ret;
}
};

} // namespace semver

+ 114
- 0
src/semver/ident.cpp View File

@@ -0,0 +1,114 @@
#include "./ident.hpp"

#include <cassert>
#include <charconv>

using namespace semver;

ident::ident(std::string_view str) {
const auto str_begin = str.data();
auto ptr = str_begin;
const auto str_end = str_begin + str.size();

bool any_alpha = false;
if (ptr == str_end) {
throw invalid_ident(std::string(str));
}
for (; ptr != str_end; ++ptr) {
any_alpha = any_alpha || (*ptr == '-' || std::isalpha(*ptr));
if (!std::isalnum(*ptr) && *ptr != '-') {
break;
}
}
if (ptr != str_end) {
throw invalid_ident(std::string(str));
}

_str.assign(str_begin, ptr);

if (any_alpha) {
_kind = ident_kind::alphanumeric;
} else if (_str.size() > 1 && _str[0] == '0') {
_kind = ident_kind::digits;
} else {
_kind = ident_kind::numeric;
// Check that the integer is representable on this system
std::uint64_t n;
auto res = std::from_chars(str_begin, str_end, n);
if (res.ptr != str_end) {
throw invalid_ident(_str);
}
}
}

std::vector<ident> ident::parse_dotted_seq(const std::string_view s) {
std::vector<ident> acc;
auto remaining = s;

while (!remaining.empty()) {
auto next_dot = remaining.find('.');
auto id_sub = remaining.substr(0, next_dot);
if (id_sub.empty()) {
throw invalid_ident(std::string(s));
}
acc.emplace_back(id_sub);
if (next_dot == remaining.npos) {
break;
}
remaining = remaining.substr(next_dot + 1);
if (remaining.empty()) {
throw invalid_ident(std::string(s));
}
}
if (acc.empty()) {
throw invalid_ident(std::string(s));
}
return acc;
}

order semver::compare(ident_kind lhs, ident_kind rhs) noexcept {
assert(lhs != ident_kind::digits && "Ordering with digit identifiers is undefined");
assert(rhs != ident_kind::digits && "Ordering with digit identifiers is undefined");
if (lhs == rhs) {
return order::equivalent;
} else if (lhs == ident_kind::numeric) {
// numeric is less than alnum
return order::less;
} else {
return order::greater;
}
}

order semver::compare(const ident& lhs, const ident& rhs) noexcept {
auto ord = compare(lhs.kind(), rhs.kind());
if (ord != order::equivalent) {
return ord;
}
assert(lhs.kind() == rhs.kind() && "[semver library bug]");
if (lhs.kind() == ident_kind::numeric) {
auto as_int = [](auto& str) {
std::uint64_t value;
auto fc_res = std::from_chars(str.data(), str.data() + str.size(), value);
assert(fc_res.ptr == str.data() + str.size());
return value;
};
auto lhs_num = as_int(lhs.string());
auto rhs_num = as_int(rhs.string());
if (lhs_num == rhs_num) {
return order::equivalent;
} else if (lhs_num < rhs_num) {
return order::less;
} else {
return order::greater;
}
}
assert(lhs.kind() == ident_kind::alphanumeric && "[semver library bug]");
auto comp = lhs.string().compare(rhs.string());
if (comp == 0) {
return order::equivalent;
} else if (comp < 0) {
return order::less;
} else {
return order::greater;
}
}

+ 62
- 0
src/semver/ident.hpp View File

@@ -0,0 +1,62 @@
#pragma once

#include <semver/order.hpp>

#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>

namespace semver {

class invalid_ident : public std::runtime_error {
std::string _str;

public:
invalid_ident(std::string s)
: std::runtime_error::runtime_error("Invalid metadata identifier: " + s)
, _str(s) {}

auto& string() const noexcept { return _str; }
};

enum class ident_kind {
alphanumeric,
numeric,
digits,
};

order compare(ident_kind lhs, ident_kind rhs) noexcept;

class ident;
order compare(const ident& lhs, const ident& rhs) noexcept;

class ident {
std::string _str;
ident_kind _kind;

public:
explicit ident(std::string_view str);

auto kind() const noexcept { return _kind; }
const auto& string() const noexcept { return _str; }

#define DEF_OP(op, expr) \
inline friend bool operator op(const ident& lhs, const ident& rhs) noexcept { \
auto o = compare(lhs, rhs); \
return (expr); \
} \
static_assert(true)

DEF_OP(==, (o == order::equivalent));
DEF_OP(!=, (o != order::equivalent));
DEF_OP(<, (o == order::less));
DEF_OP(>, (o == order::greater));
DEF_OP(<=, (o == order::less || o == order::equivalent));
DEF_OP(>=, (o == order::greater || o == order::equivalent));
#undef DEF_OP

static std::vector<ident> parse_dotted_seq(std::string_view s);
};

} // namespace semver

+ 89
- 0
src/semver/ident.test.cpp View File

@@ -0,0 +1,89 @@
#include <semver/ident.hpp>

#include <catch2/catch.hpp>

TEST_CASE("Parse identifiers") {
struct case_ {
std::string str;
semver::ident_kind expect_kind;
};
case_ cases[] = {
{"foo", semver::ident_kind::alphanumeric},
{"12", semver::ident_kind::numeric},
{"03412", semver::ident_kind::digits},
{"0", semver::ident_kind::numeric},
{"00", semver::ident_kind::digits},
};
for (auto [str, expect] : cases) {
INFO("Checking parsing of '" << str << "'");
auto id = semver::ident(str);
CHECK(id.string() == str);
CHECK(id.kind() == expect);
}
}

TEST_CASE("Invalid identifiers") {
#define BAD_IDENT(x) CHECK_THROWS_AS(semver::ident(x), semver::invalid_ident)
BAD_IDENT("");
BAD_IDENT("=");
BAD_IDENT("asdf-_");
BAD_IDENT(".");
BAD_IDENT(" ");
BAD_IDENT("124a[");
}

TEST_CASE("Ident comparison") {
using semver::order;
struct comp_test {
std::string lhs;
std::string rhs;
order expect_ordering;
};
comp_test comparisons[] = {
{"foo", "bar", order::greater},
{"foo", "foo", order::equivalent},
{"bar", "foo", order::less},
{"12", "333", order::less},
{"fooood", "3", order::greater},
{"0", "0", order::equivalent},
{"34", "f", order::less},
{"aaaaaaaaaaa", "z", order::less},
};
for (auto [lhs, rhs, expect] : comparisons) {
auto lhs_id = semver::ident(lhs);
auto rhs_id = semver::ident(rhs);
INFO("Comparing '" << lhs << "' to '" << rhs << "'");
auto result = semver::compare(lhs_id, rhs_id);
CHECK(result == expect);
}
}

TEST_CASE("Parse dotted sequence") {
struct case_ {
std::string str;
std::vector<std::string> expected;
};
case_ cases[] = {
{"foo", {"foo"}},
{"foo.bar", {"foo", "bar"}},
};
for (auto& [str, expected] : cases) {
INFO("Parsing dotted-ident-sequence '" << str << "'");
auto actual = semver::ident::parse_dotted_seq(str);
REQUIRE(actual.size() == expected.size());
}
}

TEST_CASE("Invalid dotted sequence") {
std::string_view bad_seqs[] = {
"",
".",
".foo",
"foo.bar.",
"foo..bar",
};
for (auto s : bad_seqs) {
INFO("Checking bad ident sequence '" << s << "'");
CHECK_THROWS_AS(semver::ident::parse_dotted_seq(s), semver::invalid_ident);
}
}

+ 11
- 0
src/semver/order.hpp View File

@@ -0,0 +1,11 @@
#pragma once

namespace semver {

enum class order {
less = -1,
equivalent = 0,
greater = 1,
};

} // namespace semver

+ 40
- 0
src/semver/prerelease.cpp View File

@@ -0,0 +1,40 @@
#include "./prerelease.hpp"

using namespace semver;

prerelease prerelease::parse(std::string_view s) {
auto ids = ident::parse_dotted_seq(s);
for (auto& id : ids) {
if (id.kind() == ident_kind::digits) {
throw invalid_ident("Prerelease strings may not have plain-digit identifiers: "
+ std::string(s));
}
}
prerelease ret;
ret._ids = std::move(ids);
return ret;
}

order semver::compare(const prerelease& lhs, const prerelease& rhs) noexcept {
auto lhs_iter = lhs.idents().cbegin();
auto rhs_iter = rhs.idents().cbegin();
const auto lhs_end = lhs.idents().cend();
const auto rhs_end = rhs.idents().cend();

for (; lhs_iter != lhs_end && rhs_iter != rhs_end; ++lhs_iter, ++rhs_iter) {
auto ord = compare(*lhs_iter, *rhs_iter);
if (ord != order::equivalent) {
return ord;
}
}
if (lhs_iter != lhs_end) {
// Left-hand is longer
return order::greater;
} else if (rhs_iter != rhs_end) {
// Right-hand is longer
return order::less;
} else {
// They are equivalent
return order::equivalent;
}
}

+ 46
- 0
src/semver/prerelease.hpp View File

@@ -0,0 +1,46 @@
#pragma once

#include <semver/ident.hpp>
#include <semver/order.hpp>

#include <stdexcept>
#include <string_view>
#include <vector>

namespace semver {

class prerelease;
order compare(const prerelease& lhs, const prerelease& rhs) noexcept;

class prerelease {
std::vector<ident> _ids;

public:
prerelease() = default;

[[nodiscard]] bool empty() const noexcept { return _ids.empty(); }

void add_ident(ident id);
void add_ident(std::string_view str) { add_ident(ident(str)); }

auto& idents() const noexcept { return _ids; }

static prerelease parse(std::string_view str);

#define DEF_OP(op, expr) \
inline friend bool operator op(const prerelease& lhs, const prerelease& rhs) noexcept { \
auto o = compare(lhs, rhs); \
return (expr); \
} \
static_assert(true)

DEF_OP(==, (o == order::equivalent));
DEF_OP(!=, (o != order::equivalent));
DEF_OP(<, (o == order::less));
DEF_OP(>, (o == order::greater));
DEF_OP(<=, (o == order::less || o == order::equivalent));
DEF_OP(>=, (o == order::greater || o == order::equivalent));
#undef DEF_OP
};

} // namespace semver

+ 48
- 0
src/semver/prerelease.test.cpp View File

@@ -0,0 +1,48 @@
#include "./prerelease.hpp"

#include <catch2/catch.hpp>

TEST_CASE("Parse a prerelease") {
auto pre = semver::prerelease::parse("foo.bar");
CHECK(pre.idents() == semver::ident::parse_dotted_seq("foo.bar"));
pre = pre.parse("foo.0");
CHECK(pre.idents() == semver::ident::parse_dotted_seq("foo.0"));
}

TEST_CASE("Invalid prereleases") {
std::string_view bad_tags[] = {
"foo.", // Just plain bad
"foo.122.03", // Leading zero
};
for (auto bad : bad_tags) {
CHECK_THROWS_AS(semver::prerelease::parse(bad), semver::invalid_ident);
}
}

TEST_CASE("Compare prereleases") {
using semver::order;
struct case_ {
std::string lhs;
std::string rhs;
order expected_ord;
};

case_ cases[] = {
{"foo.bar", "foo.bar", order::equivalent},
{"foo", "foo.bar", order::less},
{"foo.bar", "foo", order::greater},
{"foo", "foo", order::equivalent},
{"foo.1", "foo.bar", order::less}, // numeric is less
{"foo.foo", "foo.bar", order::greater},
{"alpha", "beta", order::less},
{"alpha.1", "alpha.2", order::less},
};

for (auto& [lhs, rhs, exp] : cases) {
INFO("Comparing prerelease '" << lhs << "' to '" << rhs << "'");
auto lhs_pre = semver::prerelease::parse(lhs);
auto rhs_pre = semver::prerelease::parse(rhs);
auto actual = semver::compare(lhs_pre, rhs_pre);
CHECK(actual == exp);
}
}

+ 71
- 9
src/semver/version.cpp View File

@@ -14,38 +14,57 @@ namespace {
version parse(const char* ptr, std::size_t size) {
version ret;
// const auto str_begin = ptr;
const auto str_end = ptr + size;
const auto str_begin = ptr;
const auto str_end = ptr + size;
auto get_str = [=] { return std::string(str_begin, size); };
auto cur_off = [&] { return ptr - str_begin; };

std::from_chars_result fc_res;
auto did_error = [&](int elem) { return fc_res.ec == std::errc::invalid_argument || elem < 0; };

// Parse major
fc_res = std::from_chars(ptr, str_end, ret.major);
ptr = fc_res.ptr;
if (did_error(ret.major) || fc_res.ptr == str_end || *fc_res.ptr != '.') {
throw invalid_version(0);
throw invalid_version(get_str(), cur_off());
}

// Parse minor
ptr = fc_res.ptr + 1;
fc_res = std::from_chars(ptr, str_end, ret.minor);
ptr = fc_res.ptr;
if (did_error(ret.minor) || fc_res.ptr == str_end || *fc_res.ptr != '.') {
throw invalid_version(ptr - str_end);
throw invalid_version(get_str(), cur_off());
}

// Parse patch
ptr = fc_res.ptr + 1;
fc_res = std::from_chars(ptr, str_end, ret.patch);
ptr = fc_res.ptr;
if (did_error(ret.patch)) {
throw invalid_version(ptr - str_end);
throw invalid_version(get_str(), cur_off());
}

if (fc_res.ptr != str_end) {
assert(false && "More complex version numbers are not ready yet!");
throw invalid_version(-42);
auto remaining = std::string_view(ptr, str_end - ptr);
if (!remaining.empty() && remaining[0] == '-') {
auto plus_pos = remaining.find('+');
auto prerelease_str = remaining.substr(1, plus_pos - 1);
ret.prerelease = prerelease::parse(prerelease_str);
remaining = remaining.substr(prerelease_str.size() + 1);
}

if (!remaining.empty() && remaining[0] == '+') {
auto bmeta_str = remaining.substr(1);
ret.build_metadata = build_metadata::parse(bmeta_str);
remaining = remaining.substr(bmeta_str.size() + 1);
}

if (!remaining.empty()) {
throw invalid_version(get_str(), remaining.data() - str_begin);
}

return ret;
}
} // namespace

} // namespace

@@ -72,5 +91,48 @@ std::string version::to_string() const noexcept {
*buf_ptr++ = '.';
conv_one(patch);

return std::string(buf_begin, (buf_ptr - buf_begin));
auto main_ver = std::string(buf_begin, (buf_ptr - buf_begin));

auto format_ids = [](auto& ids) {
std::string acc;
auto it = ids.cbegin();
auto stop = ids.cend();
while (it != stop) {
acc += it->string();
++it;
if (it != stop) {
acc += ".";
}
}
return acc;
};

if (!prerelease.empty()) {
main_ver += "-" + format_ids(prerelease.idents());
}
if (!build_metadata.empty()) {
main_ver += "+" + format_ids(build_metadata.idents());
}
return main_ver;
}

order semver::compare(const version& lhs, const version& rhs) noexcept {
auto lhs_tup = std::tie(lhs.major, lhs.minor, lhs.patch);
auto rhs_tup = std::tie(rhs.major, rhs.minor, rhs.patch);
if (lhs_tup < rhs_tup) {
return order::less;
} else if (lhs_tup > rhs_tup) {
return order::greater;
} else {
if (!lhs.is_prerelease() && rhs.is_prerelease()) {
// No prerelease is greater than any prerelease
return order::greater;
} else if (lhs.is_prerelease() && !rhs.is_prerelease()) {
// A prerelease version is lesser than any non-prerelease version
return order::less;
} else {
// Compare the prerelease tags
return compare(lhs.prerelease, rhs.prerelease);
}
}
}

+ 34
- 16
src/semver/version.hpp View File

@@ -1,5 +1,8 @@
#pragma once

#include <semver/build_metadata.hpp>
#include <semver/prerelease.hpp>

#include <stdexcept>
#include <string>
#include <string_view>
@@ -9,37 +12,52 @@
namespace semver {

class invalid_version : public std::runtime_error {
std::string _string;
std::ptrdiff_t _offset = 0;

public:
invalid_version(std::ptrdiff_t n)
: runtime_error("Invalid version number")
invalid_version(std::string string, std::ptrdiff_t n)
: runtime_error("Invalid semantic version: " + string)
, _string(string)
, _offset(n) {}

auto offset() const noexcept { return _offset; }
auto& string() const noexcept { return _string; }
auto offset() const noexcept { return _offset; }
};

class version;
order compare(const version& lhs, const version& rhs) noexcept;

struct version {
int major = 0;
int minor = 0;
int patch = 0;
// Prerelease tag is optional:
class prerelease prerelease = {};
// Build metadata is optional:
class build_metadata build_metadata = {};

static version parse(std::string_view s);

std::string to_string() const noexcept;

auto tie() const noexcept { return std::tie(major, minor, patch); }
auto tie() noexcept { return std::tie(major, minor, patch); }
bool is_prerelease() const noexcept { return !prerelease.empty(); }

#define DEF_OP(op, expr) \
inline friend bool operator op(const version& lhs, const version& rhs) noexcept { \
auto o = compare(lhs, rhs); \
return (expr); \
} \
static_assert(true)

DEF_OP(==, (o == order::equivalent));
DEF_OP(!=, (o != order::equivalent));
DEF_OP(<, (o == order::less));
DEF_OP(>, (o == order::greater));
DEF_OP(<=, (o == order::less || o == order::equivalent));
DEF_OP(>=, (o == order::greater || o == order::equivalent));
#undef DEF_OP

friend inline std::string to_string(const version& ver) noexcept { return ver.to_string(); }
};

inline bool operator!=(const version& lhs, const version& rhs) noexcept {
return lhs.tie() != rhs.tie();
}

inline bool operator<(const version& lhs, const version& rhs) noexcept {
return lhs.tie() < rhs.tie();
}

inline std::string to_string(const version& ver) noexcept { return ver.to_string(); }

} // namespace semver

+ 78
- 0
src/semver/version.test.cpp View File

@@ -13,4 +13,82 @@ TEST_CASE("Parsing") {
CHECK(v1.to_string() == "1.2.55");
v1.major = 999999;
CHECK(v1.to_string() == "999999.2.55");

v1 = semver::version::parse("1.2.3-r1");
CHECK(v1.prerelease == semver::prerelease::parse("r1"));
CHECK(v1.to_string() == "1.2.3-r1");

v1 = semver::version::parse("1.2.3-3");
CHECK(v1.prerelease == semver::prerelease::parse("3"));

v1 = semver::version::parse("1.2.3-0");
CHECK(v1.prerelease == semver::prerelease::parse("0"));

v1 = semver::version::parse("1.2.3--fasdf");
CHECK(v1.prerelease == semver::prerelease::parse("-fasdf"));

v1 = semver::version::parse("1.2.3-foo.bar");
CHECK(v1.prerelease == semver::prerelease::parse("foo.bar"));

v1 = semver::version::parse("1.2.3-foo.bar+cats");
CHECK(v1.prerelease == semver::prerelease::parse("foo.bar"));
CHECK(v1.build_metadata.idents() == semver::ident::parse_dotted_seq("cats"));
}

TEST_CASE("Compare versions") {
using semver::order;
struct case_ {
std::string_view lhs;
std::string_view rhs;
order expect_ord;
};

case_ cases[] = {
{"1.2.3", "1.2.3", order::equivalent},
{"1.2.3-alpha", "1.2.3-alpha", order::equivalent},
{"1.2.3-alpha", "1.2.3", order::less},
{"1.2.3-alpha.1", "1.2.3-beta", order::less},
{"1.2.3-alpha.1", "1.2.3-alpha.2", order::less},
{"1.2.3-alpha.4", "1.2.3-alpha.2", order::greater},
{"1.2.1", "1.2.2-alpha.2", order::less},
{"1.2.1", "1.2.1+foo", order::equivalent}, // Build metadata has no effect
};

for (auto [lhs, rhs, exp] : cases) {
INFO("Compare version '" << lhs << "' to '" << rhs << "'");
auto lhs_ver = semver::version::parse(lhs);
auto rhs_ver = semver::version::parse(rhs);
auto actual = semver::compare(lhs_ver, rhs_ver);
CHECK(actual == exp);
}
}

TEST_CASE("Invalid versions") {
struct invalid_version {
std::string str;
int bad_offset;
};
invalid_version versions[] = {
{"", 0},
{"1.", 2},
{"1.2", 3},
{"1.2.", 4},
{"1.e", 2},
{"lol", 0},
{"1e.3.1", 1},
{"1.2.5-", 6},
{"1.2.5-02", 6},
{"1.2.3-1..3", 8},
};
for (auto&& [str, bad_offset] : versions) {
INFO("Checking for failure while parsing bad version string '" << str << "'");
try {
auto ver = semver::version::parse(str);
FAIL_CHECK("Parsing didn't throw! Produced version: " << ver.to_string());
} catch (const semver::invalid_version& e) {
CHECK(e.offset() == bad_offset);
} catch (const semver::invalid_ident&) {
// Nothing to check, but a valid error
}
}
}

Loading…
Cancel
Save