Skip to content

Commit 0dc8a11

Browse files
authored
slightly safer and more correct (#17)
* slightly safer and more correct * lint
1 parent 8ff5f62 commit 0dc8a11

File tree

4 files changed

+146
-58
lines changed

4 files changed

+146
-58
lines changed

CMakeLists.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ include(GNUInstallDirs)
1313
include(FetchContent)
1414
FetchContent_Declare(
1515
googletest
16-
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
16+
URL https://github.com/google/googletest/archive/52eb810.zip
17+
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
1718
)
1819
# For Windows: Prevent overriding the parent project's compiler/linker settings
1920
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
@@ -38,4 +39,4 @@ install(
3839
NAMELINK_COMPONENT version_weaver_development
3940
ARCHIVE COMPONENT version_weaver_development
4041
INCLUDES DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
41-
)
42+
)

include/version_weaver.h

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ namespace version_weaver {
1010
// https://semver.org/#does-semver-have-a-size-limit-on-the-version-string
1111
static constexpr size_t MAX_VERSION_LENGTH = 256;
1212

13+
// Validate a version string.
14+
// A valid version string MUST be a non-empty string of characters that
15+
// conform to the grammar:
16+
// version ::= major '.' minor '.' patch [ '-' pre-release ] [ '+' build ]
17+
// major ::= non-zero-digit *digit
18+
// minor ::= non-zero-digit *digit
19+
// patch ::= non-zero-digit *digit
20+
// pre-release ::= identifier *('.' identifier)
21+
// identifier ::= non-zero-digit *digit / alpha / alpha-numeric
22+
// build ::= identifier *('.' identifier)
23+
// non-zero-digit ::= '1' / '2' / '3' / '4' / '5' / '6' / '7' / '8' / '9'
24+
// digit ::= '0' / non-zero-digit
1325
bool validate(std::string_view version);
1426

1527
bool satisfies(std::string_view version, std::string_view range);
@@ -49,11 +61,27 @@ struct version {
4961
// Examples: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85,
5062
// 1.0.0+21AF26D3----117B344092BD.
5163
std::optional<std::string_view> build;
64+
65+
inline operator std::string() const {
66+
std::string result = std::string(major) + "." + std::string(minor) + "." +
67+
std::string(patch);
68+
if (pre_release.has_value()) {
69+
result += "-" + std::string(pre_release.value());
70+
}
71+
if (build.has_value()) {
72+
result += "+" + std::string(build.value());
73+
}
74+
return result;
75+
}
5276
};
5377

5478
enum parse_error {
5579
VERSION_LARGER_THAN_MAX_LENGTH,
5680
INVALID_INPUT,
81+
INVALID_MAJOR,
82+
INVALID_MINOR,
83+
INVALID_PATCH,
84+
INVALID_RELEASE_TYPE,
5785
};
5886

5987
// This will return a cleaned and trimmed semver version.
@@ -75,14 +103,29 @@ enum release_type {
75103
// - RELEASE
76104
};
77105

78-
enum inc_error {
79-
INVALID_MAJOR,
80-
INVALID_MINOR,
81-
INVALID_PATCH,
82-
INVALID_RELEASE_TYPE,
83-
};
106+
// Increment the version according to the provided release type.
107+
std::expected<std::string, parse_error> inc(version input,
108+
release_type release_type);
109+
110+
inline std::expected<std::string, parse_error> increment(
111+
std::string_view input, release_type release_type) {
112+
auto parts = parse(input);
113+
if (!parts.has_value()) {
114+
return std::unexpected(parts.error());
115+
}
116+
return inc(parts.value(), release_type);
117+
}
118+
119+
inline std::expected<std::string, parse_error> operator+(
120+
std::string_view lhs, const release_type& rhs) {
121+
return increment(lhs, rhs);
122+
}
123+
124+
inline std::expected<std::string, parse_error> operator+(
125+
const std::string& lhs, const release_type& rhs) {
126+
return operator+(std::string_view(lhs), rhs);
127+
}
84128

85-
std::expected<version, inc_error> inc(version version, release_type release_type);
86129
} // namespace version_weaver
87130

88131
// https://semver.org/#spec-item-11

src/version_weaver.cpp

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include <algorithm>
33
#include <cctype>
44
#include <regex>
5+
#include <charconv>
56

67
namespace version_weaver {
78
bool validate(std::string_view version) { return parse(version).has_value(); }
@@ -33,48 +34,51 @@ std::optional<std::string> coerce(const std::string& version) {
3334

3435
std::string minimum(std::string_view range) { return ""; }
3536

36-
std::expected<version, inc_error> inc(version input, release_type release_type) {
37+
std::expected<std::string, parse_error> inc(version input,
38+
release_type release_type) {
3739
switch (release_type) {
3840
case MAJOR: {
3941
int major_int;
40-
try {
41-
major_int = std::stoi(std::string(input.major));
42-
} catch (...) {
43-
return std::unexpected(inc_error::INVALID_MAJOR);
42+
auto [ptr, ec] =
43+
std::from_chars(input.major.data(),
44+
input.major.data() + input.major.size(), major_int);
45+
if (ec != std::errc()) {
46+
return std::unexpected(parse_error::INVALID_MAJOR);
4447
}
4548
auto incremented_major_int = major_int + 1;
46-
std::string_view incremented_major(std::to_string(incremented_major_int));
47-
return version_weaver::version{incremented_major, "0", "0"};
49+
auto major = std::to_string(incremented_major_int);
50+
auto new_version = version_weaver::version{major, "0", "0"};
51+
return new_version;
4852
}
4953
case MINOR: {
5054
int minor_int;
51-
try {
52-
minor_int = std::stoi(std::string(input.minor));
53-
} catch (...) {
54-
return std::unexpected(inc_error::INVALID_MINOR);
55+
auto [ptr, ec] =
56+
std::from_chars(input.minor.data(),
57+
input.minor.data() + input.minor.size(), minor_int);
58+
if (ec != std::errc()) {
59+
return std::unexpected(parse_error::INVALID_MINOR);
5560
}
5661
auto incremented_minor_int = minor_int + 1;
57-
std::string_view incremented_minor(std::to_string(incremented_minor_int));
58-
return version_weaver::version{input.major, incremented_minor, "0"};
62+
return version_weaver::version{
63+
input.major, std::to_string(incremented_minor_int), "0"};
5964
}
6065
case PATCH: {
61-
auto dash_post = input.patch.find("-");
62-
if (dash_post != std::string::npos) {
63-
auto incremented_patch = input.patch.substr(0, dash_post);
64-
return version_weaver::version{input.major, input.minor, incremented_patch};
66+
if (input.pre_release) {
67+
return version_weaver::version{input.major, input.minor, input.patch};
6568
}
6669
int patch_int;
67-
try {
68-
patch_int = std::stoi(std::string(input.patch));
69-
} catch (...) {
70-
return std::unexpected(inc_error::INVALID_PATCH);
70+
auto [ptr, ec] =
71+
std::from_chars(input.patch.data(),
72+
input.patch.data() + input.patch.size(), patch_int);
73+
if (ec != std::errc()) {
74+
return std::unexpected(parse_error::INVALID_PATCH);
7175
}
7276
auto incremented_patch_int = patch_int + 1;
73-
std::string_view incremented_patch(std::to_string(incremented_patch_int));
74-
return version_weaver::version{input.major, input.minor, incremented_patch};
77+
return version_weaver::version{input.major, input.minor,
78+
std::to_string(incremented_patch_int)};
7579
}
7680
default:
77-
return std::unexpected(inc_error::INVALID_RELEASE_TYPE);
81+
return std::unexpected(parse_error::INVALID_RELEASE_TYPE);
7882
}
7983
}
8084

@@ -162,6 +166,7 @@ std::expected<version, parse_error> parse(std::string_view input) {
162166
return std::unexpected(parse_error::INVALID_INPUT);
163167
}
164168
version.patch = patch;
169+
165170
if (dot_iterator == std::string_view::npos) {
166171
return version;
167172
}

tests/basictests.cpp

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ std::vector<TestData> clean_values = {
7171
TEST(basictests, clean) {
7272
for (const auto& [input, expected] : clean_values) {
7373
auto cleaned_result = version_weaver::clean(input);
74-
std::printf("input: %s\n", input.c_str());
7574
ASSERT_EQ(cleaned_result.has_value(), expected.has_value());
7675
if (cleaned_result.has_value()) {
7776
ASSERT_EQ(cleaned_result->major, expected->major);
@@ -186,43 +185,83 @@ TEST(basictests, coerce) {
186185
}
187186

188187
using IncTestData = std::tuple<
189-
version_weaver::version,
190-
version_weaver::release_type,
191-
std::expected<version_weaver::version, version_weaver::inc_error>
192-
>;
188+
version_weaver::version, std::string, version_weaver::release_type,
189+
std::string,
190+
std::expected<version_weaver::version, version_weaver::parse_error>>;
193191

194192
std::vector<IncTestData> inc_values = {
195-
{version_weaver::version{"1", "2", "3"}, version_weaver::release_type::MAJOR, version_weaver::version{"2", "0", "0"}},
196-
{version_weaver::version{"1", "2", "3"}, version_weaver::release_type::MINOR, version_weaver::version{"1", "3", "0"}},
197-
{version_weaver::version{"1", "2", "3"}, version_weaver::release_type::PATCH, version_weaver::version{"1", "2", "4"}},
198-
{version_weaver::version{"1", "2", "3tag"}, version_weaver::release_type::MAJOR, version_weaver::version{"2", "0", "0"}},
199-
{version_weaver::version{"1", "2", "3-tag"}, version_weaver::release_type::MAJOR, version_weaver::version{"2", "0", "0"}},
200-
{version_weaver::version{"1", "2", "3"}, static_cast<version_weaver::release_type>(-1), std::unexpected(version_weaver::inc_error::INVALID_RELEASE_TYPE)},
201-
{version_weaver::version{"1", "2", "0-0"}, version_weaver::release_type::PATCH, version_weaver::version{"1", "2", "0"}},
202-
{version_weaver::version{"fake"}, version_weaver::release_type::MAJOR, std::unexpected(version_weaver::inc_error::INVALID_MAJOR)},
203-
{version_weaver::version{"1", "2", "3-4"}, version_weaver::release_type::MAJOR, version_weaver::version{"2", "0", "0"}},
204-
{version_weaver::version{"1", "2", "3-4"}, version_weaver::release_type::MINOR, version_weaver::version{"1", "3", "0"}},
205-
{version_weaver::version{"1", "2", "3-4"}, version_weaver::release_type::PATCH, version_weaver::version{"1", "2", "3"}},
206-
{version_weaver::version{"1", "2", "3-alpha.0.beta"}, version_weaver::release_type::MAJOR, version_weaver::version{"2", "0", "0"}},
207-
{version_weaver::version{"1", "2", "3-alpha.0.beta"}, version_weaver::release_type::MINOR, version_weaver::version{"1", "3", "0"}},
208-
{version_weaver::version{"1", "2", "3-alpha.0.beta"}, version_weaver::release_type::PATCH, version_weaver::version{"1", "2", "3"}},
193+
{version_weaver::version{"1", "2", "3"}, "1.2.3",
194+
version_weaver::release_type::MAJOR, "2.0.0",
195+
version_weaver::version{"2", "0", "0"}},
196+
{version_weaver::version{"1", "2", "3"}, "1.2.3",
197+
version_weaver::release_type::MINOR, "1.3.0",
198+
version_weaver::version{"1", "3", "0"}},
199+
{version_weaver::version{"1", "2", "3"}, "1.2.3",
200+
version_weaver::release_type::PATCH, "1.2.4",
201+
version_weaver::version{"1", "2", "4"}},
202+
{version_weaver::version{"1", "2", "3", "tag"}, "1.2.3-tag",
203+
version_weaver::release_type::MAJOR, "2.0.0",
204+
version_weaver::version{"2", "0", "0"}},
205+
{version_weaver::version{"1", "2", "3"}, "1.2.3",
206+
static_cast<version_weaver::release_type>(-1), "",
207+
std::unexpected(version_weaver::parse_error::INVALID_RELEASE_TYPE)},
208+
{version_weaver::version{"1", "2", "0", "0"}, "1.2.0-0",
209+
version_weaver::release_type::PATCH, "1.2.0",
210+
version_weaver::version{"1", "2", "0"}},
211+
{version_weaver::version{"fake"}, "fake",
212+
version_weaver::release_type::MAJOR, "",
213+
std::unexpected(version_weaver::parse_error::INVALID_MAJOR)},
214+
{version_weaver::version{"1", "2", "3", "4"}, "1.2.3-4",
215+
version_weaver::release_type::MAJOR, "2.0.0",
216+
version_weaver::version{"2", "0", "0"}},
217+
{version_weaver::version{"1", "2", "3", "4"}, "1.2.3-4",
218+
version_weaver::release_type::MINOR, "1.3.0",
219+
version_weaver::version{"1", "3", "0"}},
220+
{version_weaver::version{"1", "2", "3", "4"}, "1.2.3-4",
221+
version_weaver::release_type::PATCH, "1.2.3",
222+
version_weaver::version{"1", "2", "3"}},
223+
{version_weaver::version{"1", "2", "3", "alpha.0.beta"},
224+
"1.2.3-alpha.0.beta", version_weaver::release_type::MAJOR, "2.0.0",
225+
version_weaver::version{"2", "0", "0"}},
226+
{version_weaver::version{"1", "2", "3", "alpha.0.beta"},
227+
"1.2.3-alpha.0.beta", version_weaver::release_type::MINOR, "1.3.0",
228+
version_weaver::version{"1", "3", "0"}},
229+
{version_weaver::version{"1", "2", "3", "alpha.0.beta"},
230+
"1.2.3-alpha.0.beta", version_weaver::release_type::PATCH, "1.2.3",
231+
version_weaver::version{"1", "2", "3"}},
209232
};
210233

211234
TEST(basictests, inc) {
212-
for (const auto& [input, release_type, expected] : inc_values) {
213-
auto incremented = version_weaver::inc(input, release_type);
214-
215-
ASSERT_EQ(incremented.has_value(), expected.has_value());
216-
if (incremented.has_value()) {
235+
for (const auto& [input, inputstr, release_type, str, expected] :
236+
inc_values) {
237+
auto incremented_str = version_weaver::inc(input, release_type);
238+
ASSERT_EQ(incremented_str.has_value(), expected.has_value());
239+
if (incremented_str.has_value()) {
240+
ASSERT_EQ(*incremented_str, str);
241+
auto incremented = version_weaver::parse(*incremented_str);
242+
ASSERT_TRUE(incremented.has_value());
217243
ASSERT_EQ(incremented->major, expected->major);
218244
ASSERT_EQ(incremented->minor, expected->minor);
219245
ASSERT_EQ(incremented->patch, expected->patch);
220246
ASSERT_EQ(incremented->pre_release, expected->pre_release);
221247
ASSERT_EQ(incremented->build, expected->build);
222248
} else {
223-
ASSERT_EQ(incremented.error(), expected.error());
249+
ASSERT_EQ(incremented_str.error(), expected.error());
224250
}
225251
}
226252

227253
SUCCEED();
228254
}
255+
256+
TEST(basictests, plus) {
257+
for (const auto& [input, inputstr, release_type, str, expected] :
258+
inc_values) {
259+
auto incremented_str = inputstr + release_type;
260+
ASSERT_EQ(incremented_str.has_value(), expected.has_value());
261+
if (incremented_str.has_value()) {
262+
ASSERT_EQ(*incremented_str, str);
263+
}
264+
}
265+
266+
SUCCEED();
267+
}

0 commit comments

Comments
 (0)