Full semver 2.0.0 implementation and validation by a number of tests.

This commit is contained in:
Marko Zivanovic
2015-10-08 16:00:22 +02:00
parent 76506c5ffd
commit 2f5981c7d5
7 changed files with 409 additions and 342 deletions
+1 -1
View File
@@ -3,5 +3,5 @@ include_directories(
)
add_library(semver
version.cpp
Semver200_parser.cpp Semver200_comparator.cpp
)
+81
View File
@@ -0,0 +1,81 @@
/*
The MIT License (MIT)
Copyright (c) 2015 Marko Zivanovic
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "version.h"
using namespace std;
namespace {
using namespace version;
int compare_prerel_identifiers(const Prerelease_identifier& l, Prerelease_identifier& r) {
if (l.second == Identifier_type::alnum && r.second == Identifier_type::alnum) {
return l.first.compare(r.first);
} else if (l.second == Identifier_type::alnum && r.second == Identifier_type::num) {
return 1;
} else if (l.second == Identifier_type::num && r.second == Identifier_type::alnum) {
return -1;
} else if (l.second == Identifier_type::num && r.second == Identifier_type::num) {
int li = stoi(l.first);
int ri = stoi(r.first);
if (li == ri) return 0;
return li > ri ? 1 : -1;
}
throw runtime_error("unexpected identifier types: " + to_string(static_cast<int>(l.second)) + ", " +
to_string(static_cast<int>(r.second)));
}
}
namespace version {
int Semver200_comparator::compare(const Version_data& l, const Version_data& r) {
if (l.major > r.major) return 1;
if (l.major < r.major) return -1;
if (l.minor > r.minor) return 1;
if (l.minor < r.minor) return -1;
if (l.patch > r.patch) return 1;
if (l.patch < r.patch) return -1;
// release version is always higher than prerelease
if (l.prerelease_ids.empty() && !r.prerelease_ids.empty()) return 1;
if (r.prerelease_ids.empty() && !l.prerelease_ids.empty()) return -1;
// compare release by looking at each identifier: numeric ones are compared as numbers, alphanum as ASCII strings
auto shorter = l.prerelease_ids.size() <= r.prerelease_ids.size() ? l.prerelease_ids.size() : r.prerelease_ids.size();
for (size_t i = 0; i < shorter; i++) {
auto lid = l.prerelease_ids[i];
auto rid = r.prerelease_ids[i];
int cmp = compare_prerel_identifiers(lid, rid);
if (cmp != 0) return cmp;
}
// prerels are the same, to the length of the shorter one;
// if they are the same length, then version are equal, otherwise, longer wins
if (l.prerelease_ids.size() == r.prerelease_ids.size()) return 0;
return l.prerelease_ids.size() > r.prerelease_ids.size() ? 1 : -1;
}
}
+72 -46
View File
@@ -33,18 +33,19 @@ namespace {
major, minor, patch, release, build
};
using Phase_transition_hook = function<void(const string&)>;
using Phase_transition_hook = function<void(string&)>;
using Transition = tuple<const char, Parser_phase, Phase_transition_hook>;
using Validator = function<void(const string&, const char)>;
inline Transition mkx(const char c, Parser_phase p, Phase_transition_hook px) {
return make_tuple(c, p, px);
inline Transition mkx(const char c, Parser_phase p, Phase_transition_hook pth) {
return make_tuple(c, p, pth);
}
inline void process_char(const char c, Parser_phase& phase, const vector<Transition> transitions,
string& target, Validator validate) {
inline void process_char(const char c, Parser_phase& phase, Parser_phase& prev_phase,
const vector<Transition> transitions, string& target, Validator validate) {
for (const auto& t : transitions) {
if (c == get<0>(t)) {
prev_phase = phase;
phase = get<1>(t);
if (get<2>(t)) get<2>(t)(target);
return;
@@ -54,35 +55,24 @@ namespace {
target.push_back(c);
}
auto normal_version_validator = [](const string& tgt, const char c) {
inline void normal_version_validator(const string& tgt, const char c) {
if (c < '0' || c > '9') throw version::Parse_error("invalid character encountered: " + string(1, c));
if (tgt.compare(0, 1, "0") == 0) throw version::Parse_error("leading 0 not allowed");
};
}
auto release_version_validator = [](const string& tgt, const char c) {
inline void release_version_validator(const string& tgt, const char c) {
if ((c < '0' || c > '9') && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && c != '.' && c != '-')
throw version::Parse_error("invalid character encountered: " + string(1, c));
};
void validate_release_identifier(const string& id) {
if (id.find_first_not_of("0123456789") == string::npos
&& id.compare(0, 1, "0") == 0) {
throw version::Parse_error("leading 0 not allowed");
}
}
void validate_release_identifiers(const string& release) {
string id;
for (const auto& c : release) {
if (c == '.') {
validate_release_identifier(id);
id.clear();
} else {
id.push_back(c);
}
}
validate_release_identifier(id);
bool is_identifier_numeric(const string& id) {
return id.find_first_not_of("0123456789") == string::npos;
}
bool check_identifier_for_leading_0(const string& id) {
return id.length() > 1 && id[0] == '0';
}
}
namespace version {
@@ -91,18 +81,32 @@ namespace version {
string major;
string minor;
string patch;
string release;
string build;
string release_id;
string build_id;
Prerelease_identifiers release;
Build_identifiers build;
Parser_phase prev_phase;
auto process_release_component = [&](const string& t) {
if (t.empty()) throw Parse_error("version identifier cannot be empty");
if (t.compare(t.length() - 1, 1, ".") == 0) throw Parse_error("version identifier cannot be empty");
release.push_back('.');};
auto release_hook = [&](string& id) {
if (id.empty()) throw version::Parse_error("version identifier cannot be empty");
Identifier_type t = Identifier_type::alnum;
if (is_identifier_numeric(id)) {
t = Identifier_type::num;
if (check_identifier_for_leading_0(id)) {
throw Parse_error("numeric identifiers cannot have leading 0");
}
}
release.push_back(Prerelease_identifier(id, t));
id.clear();
};
auto process_build_component = [&](const string& t) {
if (t.empty()) throw Parse_error("version identifier cannot be empty");
if (t.compare(t.length() - 1, 1, ".") == 0) throw Parse_error("version identifier cannot be empty");
build.push_back('.');};
auto build_hook = [&](string& id) {
// process last token left from parsing release data
if (prev_phase == Parser_phase::release) release_hook(release_id);
if (id.empty()) throw version::Parse_error("version identifier cannot be empty");
build.push_back(id);
id.clear();
};
auto major_xs = {
mkx('.', Parser_phase::minor, {})
@@ -115,34 +119,41 @@ namespace version {
mkx('+', Parser_phase::build, {})
};
auto release_xs = {
mkx('.', Parser_phase::release, process_release_component),
mkx('.', Parser_phase::release, release_hook),
mkx('+', Parser_phase::build, {})
};
auto build_xs = {
mkx('.', Parser_phase::build, process_build_component)
mkx('.', Parser_phase::build, build_hook)
};
Parser_phase phase = Parser_phase::major;
for (const auto& c : s) {
switch (phase) {
case Parser_phase::major:
process_char(c, phase, major_xs, major, normal_version_validator);
process_char(c, phase, prev_phase, major_xs, major, normal_version_validator);
break;
case Parser_phase::minor:
process_char(c, phase, minor_xs, minor, normal_version_validator);
process_char(c, phase, prev_phase, minor_xs, minor, normal_version_validator);
break;
case Parser_phase::patch:
process_char(c, phase, patch_xs, patch, normal_version_validator);
process_char(c, phase, prev_phase, patch_xs, patch, normal_version_validator);
break;
case Parser_phase::release:
process_char(c, phase, release_xs, release, release_version_validator);
process_char(c, phase, prev_phase, release_xs, release_id, release_version_validator);
break;
case Parser_phase::build:
process_char(c, phase, build_xs, build, release_version_validator);
process_char(c, phase, prev_phase, build_xs, build_id, release_version_validator);
break;
}
}
validate_release_identifiers(release);
// Trigger appropriate hooks in order to process last token, because no phase transition was
// triggered for it.
if (phase == Parser_phase::release) {
release_hook(release_id);
}
if (phase == Parser_phase::build) {
build_hook(build_id);
}
try {
return Version_data{ stoi(major), stoi(minor), stoi(patch), release, build };
@@ -151,8 +162,23 @@ namespace version {
}
}
int Semver200_comparator::compare(const Version_data& l, const Version_data& r) {
throw 1;
ostream& operator<<(ostream& os, const Prerelease_identifier& id) {
os << id.first << "<" << static_cast<int>(id.second) << ">";
return os;
}
ostream& operator<<(ostream& os, const Prerelease_identifiers& ids) {
for (const auto& id : ids) {
os << id << ",";
}
return os;
}
ostream& operator<<(ostream& os, const Build_identifiers& ids) {
for (const auto& id : ids) {
os << id << ",";
}
return os;
}
}