diff --git a/src/univers/swift_version_range.py b/src/univers/swift_version_range.py new file mode 100644 index 00000000..6d1e8be3 --- /dev/null +++ b/src/univers/swift_version_range.py @@ -0,0 +1,57 @@ +import attr + +from univers.version_constraint import VersionConstraint +from univers.version_range import VersionRange +from univers.versions import SwiftVersion + + +@attr.s(auto_attribs=True, frozen=True) +class SwiftVersionRange(VersionRange): + expression: str + scheme: str = "swift" + version_class: type = SwiftVersion + constraints: list = attr.ib(init=False) + + def __attrs_post_init__(self): + object.__setattr__(self, "constraints", self.parse(self.expression)) + + @staticmethod + def parse(expression): + constraints = [] + # Remove quotes for parsing. + expression = expression.replace('"', "").strip() + + # Handle different range types. + if "..<" in expression: + parts = expression.split("..<") + if len(parts) == 2: + lower = SwiftVersion(parts[0].strip()) + upper = SwiftVersion(parts[1].strip()) + constraints.append(VersionConstraint(comparator=">=", version=lower)) + constraints.append(VersionConstraint(comparator="<", version=upper)) + elif "..." in expression: + parts = expression.split("...") + if len(parts) == 2: + lower = SwiftVersion(parts[0].strip()) + upper = SwiftVersion(parts[1].strip()) + constraints.append(VersionConstraint(comparator=">=", version=lower)) + constraints.append(VersionConstraint(comparator="<=", version=upper)) + else: + # Handle other cases such as 'exact:' and 'from:' prefixes. + if "exact:" in expression: + version = SwiftVersion(expression.split("exact:")[1].strip()) + constraints.append(VersionConstraint(comparator="=", version=version)) + elif "from:" in expression: + version = SwiftVersion(expression.split("from:")[1].strip()) + next_major_version = version.next_major() + constraints.append(VersionConstraint(comparator=">=", version=version)) + constraints.append(VersionConstraint(comparator="<", version=next_major_version)) + else: + # Handle a single version without any prefix. + version = SwiftVersion(expression) + constraints.append(VersionConstraint(comparator="=", version=version)) + + return constraints + + def __str__(self): + return f"vers:swift/{'|'.join([c.to_string() for c in self.constraints])}" diff --git a/src/univers/versions.py b/src/univers/versions.py index 14b3e007..328b9d09 100644 --- a/src/univers/versions.py +++ b/src/univers/versions.py @@ -703,3 +703,21 @@ def bump(self, index): LegacyOpensslVersion, AlpineLinuxVersion, ] + + +class SwiftVersion(Version): + @classmethod + def build_value(cls, string): + return semantic_version.Version(string) + + @classmethod + def is_valid(cls, string): + try: + cls.build_value(string) + return True + except ValueError: + return False + + def next_major(self): + """Increase the major version and reset minor and patch.""" + return SwiftVersion(str(self.value.next_major())) diff --git a/tests/test_swift_versions.py b/tests/test_swift_versions.py new file mode 100644 index 00000000..cf396c30 --- /dev/null +++ b/tests/test_swift_versions.py @@ -0,0 +1,38 @@ +import unittest + +from univers.swift_version_range import SwiftVersionRange +from univers.versions import SwiftVersion + + +class TestSwiftVersionRange(unittest.TestCase): + def test_exact_version(self): + vr = SwiftVersionRange('exact: "1.2.3"') + v = SwiftVersion("1.2.3") + self.assertTrue(v in vr) + + def test_from_version(self): + vr = SwiftVersionRange('from: "1.2.3"') + v1 = SwiftVersion("1.2.3") + v2 = SwiftVersion("2.0.0") + self.assertTrue(v1 in vr) + self.assertFalse(v2 in vr) + + def test_range_with_upper_bound(self): + vr = SwiftVersionRange('"1.2.3"..<"1.2.6"') + v1 = SwiftVersion("1.2.3") + v2 = SwiftVersion("1.2.6") + self.assertTrue(v1 in vr) + self.assertFalse(v2 in vr) + + def test_range_with_both_bounds(self): + vr = SwiftVersionRange('"1.2.3"..."1.2.6"') + v1 = SwiftVersion("1.2.3") + v2 = SwiftVersion("1.2.6") + v3 = SwiftVersion("1.2.7") + self.assertTrue(v1 in vr) + self.assertTrue(v2 in vr) + self.assertFalse(v3 in vr) + + +if __name__ == "__main__": + unittest.main()