Skip to content

Commit 3a7892f

Browse files
committed
Add version constraint system.
1 parent 16af966 commit 3a7892f

4 files changed

Lines changed: 485 additions & 0 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#pragma once
2+
3+
#include <optional>
4+
5+
#include <QString>
6+
7+
#include "../versioning.h"
8+
9+
namespace MOBase
10+
{
11+
class InvalidConstraintException : public Exception
12+
{
13+
public:
14+
using Exception::Exception;
15+
};
16+
17+
class VersionConstraintImpl;
18+
19+
// class representing a version constraint, e.g. "2.3.*" or ">=2.4"
20+
//
21+
class QDLLEXPORT VersionConstraint
22+
{
23+
public:
24+
// wildcard placeholder for major/minor/patch/subpatch when constructing wildcard
25+
//
26+
static constexpr int WILDCARD = -1;
27+
28+
public:
29+
// parse a constraint from the given string
30+
//
31+
static VersionConstraint parse(QString const& value, Version::ParseMode mode);
32+
33+
public:
34+
// check if the given version matches this constraint
35+
//
36+
bool matches(Version const& version) const;
37+
38+
public:
39+
~VersionConstraint();
40+
41+
private:
42+
VersionConstraint(std::shared_ptr<VersionConstraintImpl> impl);
43+
44+
std::shared_ptr<VersionConstraintImpl> m_Impl;
45+
};
46+
47+
// class representing a set of version constraints, usually from dependency
48+
// requirements e.g. "2.3.*", or ">= 2.4, <2.5"
49+
//
50+
class QDLLEXPORT VersionConstraints
51+
{
52+
public:
53+
// parse a set of constraints from the given string
54+
//
55+
static VersionConstraints parse(QString const& value, Version::ParseMode mode);
56+
57+
public:
58+
// construct a set of constraints
59+
//
60+
VersionConstraints(std::vector<VersionConstraint> constraints);
61+
62+
// check if the given version matches the set of constraints
63+
//
64+
bool matches(Version const& version) const;
65+
66+
private:
67+
std::vector<VersionConstraint> m_Constraints;
68+
};
69+
70+
} // namespace MOBase

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ set(extension_headers
4040
../include/uibase/extensions/requirements.h
4141
../include/uibase/extensions/theme.h
4242
../include/uibase/extensions/translation.h
43+
../include/uibase/extensions/versionconstraints.h
4344
)
4445
set(interface_headers
4546
../include/uibase/ifiletree.h
@@ -143,6 +144,7 @@ mo2_target_sources(uibase
143144
extension.cpp
144145
theme.cpp
145146
requirements.cpp
147+
versionconstraints.cpp
146148
)
147149

148150
mo2_target_sources(uibase

src/versionconstraints.cpp

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
#include "extensions/versionconstraints.h"
2+
3+
#include "formatters.h"
4+
5+
using VersionCompareFunction = bool (*)(MOBase::Version const& lhs,
6+
MOBase::Version const& rhs);
7+
8+
// official semver regex
9+
static const QRegularExpression s_ConstraintStrictRegEx{
10+
R"(^(?P<constraint>>=|<=|<|>|!=|==|\^|~)?\s*(?P<major>0|[1-9*]\d*)(?:\.(?P<minor>0|[1-9*]\d*)(?:\.(?P<patch>0|[1-9*]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?)?)?$)"};
11+
12+
// for MO2, to match stuff like 1.2.3rc1 or v1.2.3a1+XXX
13+
static const QRegularExpression s_ConstraintMO2RegEx{
14+
R"(^(?P<constraint>>=|<=|<|>|!=|\^|~)?\s*(?P<major>0|[1-9*]\d*)(?:\.(?P<minor>0|[1-9*]\d*)(?:\.(?P<patch>0|[1-9*]\d*)(?:\.(?P<subpatch>0|[1-9*]\d*))?(?:(?P<type>dev|a|alpha|b|beta|rc)(?P<prerelease>0|[1-9](?:[.0-9])*))?)?)?$)"};
15+
16+
// match from value to release type
17+
static const std::unordered_map<QString, MOBase::Version::ReleaseType>
18+
s_StringToRelease{{"dev", MOBase::Version::Development},
19+
{"alpha", MOBase::Version::Alpha},
20+
{"a", MOBase::Version::Alpha},
21+
{"beta", MOBase::Version::Beta},
22+
{"b", MOBase::Version::Beta},
23+
{"rc", MOBase::Version::ReleaseCandidate}};
24+
25+
#define _COMPARE_PAIR(OP) \
26+
{ \
27+
#OP, +[](MOBase::Version const& lhs, MOBase::Version const& rhs) { \
28+
return lhs OP rhs; \
29+
} \
30+
}
31+
32+
static const std::unordered_map<QString, VersionCompareFunction> s_CompareToFunction{
33+
_COMPARE_PAIR(>), _COMPARE_PAIR(>=), _COMPARE_PAIR(<),
34+
_COMPARE_PAIR(<=), _COMPARE_PAIR(!=), _COMPARE_PAIR(==)};
35+
36+
#undef _COMPARE_PAIR
37+
38+
namespace MOBase
39+
{
40+
41+
class VersionConstraintImpl
42+
{
43+
public:
44+
virtual bool matches(Version const& version) const = 0;
45+
virtual ~VersionConstraintImpl() = default;
46+
};
47+
48+
// version constraint for a range with lower bound included and upper bound excluded,
49+
// typically used for tilde, caret and wilcard constraints
50+
//
51+
class RangeVersionConstraint : public VersionConstraintImpl
52+
{
53+
public:
54+
RangeVersionConstraint(Version const& min, Version const& max)
55+
: m_Min{min}, m_Max{max}
56+
{}
57+
58+
bool matches(Version const& version) const override
59+
{
60+
return m_Min <= version && version < m_Max;
61+
}
62+
63+
private:
64+
Version m_Min, m_Max;
65+
};
66+
67+
// version constraint for inequality and equality constraint
68+
//
69+
class InequalityVersionConstraint : public VersionConstraintImpl
70+
{
71+
72+
public:
73+
InequalityVersionConstraint(Version const& target, VersionCompareFunction compare)
74+
: m_Target{target}, m_Compare{compare}
75+
{}
76+
77+
bool matches(Version const& version) const override
78+
{
79+
return m_Compare(version, m_Target);
80+
}
81+
82+
private:
83+
Version m_Target;
84+
VersionCompareFunction m_Compare;
85+
};
86+
87+
VersionConstraint VersionConstraint::parse(QString const& value,
88+
Version::ParseMode mode)
89+
{
90+
const auto& regex = mode == Version::ParseMode::SemVer ? s_ConstraintStrictRegEx
91+
: s_ConstraintMO2RegEx;
92+
93+
const auto match = regex.match(value);
94+
if (!match.hasMatch()) {
95+
throw InvalidConstraintException(
96+
QString::fromStdString(std::format("invalid constraint string: '{}'", value)));
97+
}
98+
99+
const auto constraint = match.captured("constraint");
100+
101+
const auto major_s = match.captured("major");
102+
const auto minor_s = match.captured("minor");
103+
const auto patch_s = match.captured("patch");
104+
const auto subpatch_s = match.captured("subpatch");
105+
106+
const auto wildcard =
107+
major_s == "*" || minor_s == "*" || patch_s == "*" || subpatch_s == "*";
108+
const auto tilde = match.captured("constraint") == "~";
109+
const auto caret = match.captured("constraint") == "^";
110+
111+
// cannot use wildcard with a constraint
112+
if (wildcard && !constraint.isEmpty()) {
113+
throw InvalidConstraintException(
114+
QString::fromStdString(std::format("invalid constraint string: '{}'", value)));
115+
}
116+
117+
// cannot use pre-release with wilcard, tilde or caret constraint
118+
if ((wildcard || tilde || caret) && match.hasCaptured("prerelease")) {
119+
throw InvalidConstraintException(
120+
QString::fromStdString(std::format("invalid constraint string: '{}'", value)));
121+
}
122+
123+
// if a part has a wildcard, lower part should be missing or wildcard (e.g., 2.*.3
124+
// is invalid)
125+
if (major_s == "*" && !minor_s.isEmpty() && minor_s != "*") {
126+
throw InvalidConstraintException(
127+
QString::fromStdString(std::format("invalid constraint string: '{}'", value)));
128+
}
129+
if (minor_s == "*" && !patch_s.isEmpty() && patch_s != "*") {
130+
throw InvalidConstraintException(
131+
QString::fromStdString(std::format("invalid constraint string: '{}'", value)));
132+
}
133+
if (patch_s == "*" && !subpatch_s.isEmpty() && subpatch_s != "*") {
134+
throw InvalidConstraintException(
135+
QString::fromStdString(std::format("invalid constraint string: '{}'", value)));
136+
}
137+
138+
std::vector<std::variant<int, Version::ReleaseType>> prereleases;
139+
if (mode == Version::ParseMode::SemVer) {
140+
for (auto& part : match.captured("prerelease")
141+
.split(".", Qt::SplitBehaviorFlags::SkipEmptyParts)) {
142+
// try to extract an int
143+
bool ok = true;
144+
const auto intValue = part.toInt(&ok);
145+
if (ok) {
146+
prereleases.push_back(intValue);
147+
continue;
148+
}
149+
150+
// check if we have a valid prerelease type
151+
const auto it = s_StringToRelease.find(part.toLower());
152+
if (it == s_StringToRelease.end()) {
153+
throw InvalidVersionException(
154+
QString::fromStdString(std::format("invalid prerelease type: '{}'", part)));
155+
}
156+
157+
prereleases.push_back(it->second);
158+
}
159+
} else {
160+
prereleases.push_back(s_StringToRelease.at(match.captured("type")));
161+
162+
// for version with decimal point, e.g., 2.4.1rc1.1, we split the components into
163+
// pre-release components to get {rc, 1, 1} - this works fine since {rc, 1} < {rc,
164+
// 1, 1}
165+
//
166+
for (const auto& preVersion :
167+
match.captured("prerelease").split(".", Qt::SkipEmptyParts)) {
168+
prereleases.push_back(preVersion.toInt());
169+
}
170+
}
171+
172+
constexpr auto min_int = std::numeric_limits<int>::min();
173+
constexpr auto max_int = std::numeric_limits<int>::max();
174+
175+
std::shared_ptr<VersionConstraintImpl> impl;
176+
177+
if (wildcard || caret || tilde) {
178+
179+
// you can get more information at
180+
// https://python-poetry.org/docs/dependency-specification/
181+
182+
// note that the only case where all 4 xxxOk is false is for '*'
183+
//
184+
bool majorOk, minorOk, patchOk, subpatchOk;
185+
auto major = major_s.toInt(&majorOk), minor = minor_s.toInt(&minorOk),
186+
patch = patch_s.toInt(&patchOk), subpatch = subpatch_s.toInt(&subpatchOk);
187+
188+
// the lower bound is always the actual version with missing or wildcard components
189+
// set to 0, e.g.
190+
// - 2.3.* -> >= 2.3.0
191+
// - ^1 -> >= 1.0.0
192+
// - ^0.3 -> >= 0.3.0
193+
// - ~1.2 -> >= 1.2.0
194+
const Version min = Version(major, minor, patch, subpatch);
195+
196+
// the upper bound is a bit more complicated to compute
197+
Version max = Version(max_int, max_int, max_int, max_int);
198+
199+
if (wildcard) {
200+
// for wildcard, we increment the last non-wildcard character by one
201+
//
202+
if (minorOk && patchOk) {
203+
max = Version(major, minor, patch + 1);
204+
} else if (minorOk) {
205+
max = Version(major, minor + 1, 0);
206+
} else {
207+
max = Version(major + 1, 0, 0);
208+
}
209+
} else if (caret) {
210+
// TODO: clean this...
211+
212+
if (!minorOk && !patchOk && !subpatchOk) {
213+
max = Version(major + 1, 0, 0);
214+
} else if (!patchOk && !subpatchOk) {
215+
if (major == 0) {
216+
max = Version(major, minor + 1, 0);
217+
} else {
218+
max = Version(major + 1, 0, 0);
219+
}
220+
} else if (!subpatchOk) {
221+
if (major == 0 && minor == 0) {
222+
max = Version(major, minor, patch + 1);
223+
} else if (major == 0) {
224+
max = Version(major, minor + 1, 0);
225+
} else {
226+
max = Version(major + 1, 0, 0);
227+
}
228+
} else {
229+
if (major == 0 && minor == 0 && patch == 0 && subpatch == 0) {
230+
max = min; // this creates an impossible range (>= 0, < 0), but is expected
231+
} else if (major == 0 && minor == 0 && patch == 0) {
232+
max = Version(major, minor, patch, subpatch + 1);
233+
} else if (major == 0 && minor == 0) {
234+
max = Version(major, minor, patch + 1, 0);
235+
} else if (major == 0) {
236+
max = Version(major, minor + 1, 0);
237+
} else {
238+
max = Version(major + 1, 0, 0);
239+
}
240+
}
241+
242+
} else if (tilde) {
243+
if (minorOk && patchOk && subpatchOk) {
244+
max = Version(major, minor, patch, subpatch + 1);
245+
} else if (minorOk && patchOk) {
246+
max = Version(major, minor, patch + 1);
247+
} else if (minorOk) {
248+
max = Version(major, minor + 1, 0);
249+
} else {
250+
max = Version(major + 1, 0, 0);
251+
}
252+
}
253+
254+
impl = std::make_shared<RangeVersionConstraint>(min, max);
255+
256+
} else {
257+
auto op = match.captured("constraint");
258+
if (op.isEmpty()) {
259+
op = "==";
260+
}
261+
impl = std::make_shared<InequalityVersionConstraint>(
262+
Version(major_s.toInt(), minor_s.toInt(), patch_s.toInt(), subpatch_s.toInt(),
263+
std::move(prereleases)),
264+
s_CompareToFunction.at(op));
265+
}
266+
267+
return VersionConstraint(std::move(impl));
268+
}
269+
270+
VersionConstraint::VersionConstraint(std::shared_ptr<VersionConstraintImpl> impl)
271+
: m_Impl{std::move(impl)}
272+
{}
273+
274+
VersionConstraint::~VersionConstraint() = default;
275+
276+
bool VersionConstraint::matches(Version const& version) const
277+
{
278+
return m_Impl->matches(version);
279+
}
280+
281+
VersionConstraints VersionConstraints::parse(QString const& value,
282+
Version::ParseMode mode)
283+
{
284+
std::vector<VersionConstraint> constraints;
285+
for (const auto& part : value.split(",")) {
286+
constraints.push_back(VersionConstraint::parse(part.trimmed(), mode));
287+
}
288+
return VersionConstraints(std::move(constraints));
289+
}
290+
291+
bool VersionConstraints::matches(Version const& version) const
292+
{
293+
return std::all_of(m_Constraints.begin(), m_Constraints.end(),
294+
[version](const auto& constraint) {
295+
return constraint.matches(version);
296+
});
297+
}
298+
299+
VersionConstraints::VersionConstraints(std::vector<VersionConstraint> checkers)
300+
: m_Constraints{std::move(checkers)}
301+
{}
302+
303+
} // namespace MOBase

0 commit comments

Comments
 (0)