Class: Vers::VersionRange
- Inherits:
-
Object
- Object
- Vers::VersionRange
- Defined in:
- lib/vers/version_range.rb
Constant Summary collapse
- VALIDATED_CONTAINMENT_SCHEMES =
%w[bazel cargo composer go hex npm pub pypi semver].freeze
Instance Attribute Summary collapse
-
#exclusions ⇒ Object
readonly
Returns the value of attribute exclusions.
-
#intervals ⇒ Object
readonly
Returns the value of attribute intervals.
-
#raw_constraints ⇒ Object
readonly
Returns the value of attribute raw_constraints.
-
#scheme ⇒ Object
readonly
Returns the value of attribute scheme.
Class Method Summary collapse
- .empty(scheme: nil) ⇒ Object
- .exact(version, scheme: nil) ⇒ Object
- .greater_than(version, inclusive: false, scheme: nil) ⇒ Object
- .less_than(version, inclusive: false, scheme: nil) ⇒ Object
- .unbounded(scheme: nil) ⇒ Object
Instance Method Summary collapse
- #compatible_scheme(other) ⇒ Object
- #complement ⇒ Object
- #composer_interval_contains?(interval, version) ⇒ Boolean
- #contains?(version) ⇒ Boolean
- #empty? ⇒ Boolean
- #exact_interval?(interval) ⇒ Boolean
- #exclude(version) ⇒ Object
- #excluded_version?(version, excluded) ⇒ Boolean
-
#initialize(intervals = [], raw_constraints: nil, scheme: nil, exclusions: []) ⇒ VersionRange
constructor
A new instance of VersionRange.
- #intersect(other) ⇒ Object
- #intersect_intervals(left, right, comparison_scheme) ⇒ Object
- #prerelease_allowed?(interval, version) ⇒ Boolean
- #pypi_interval_contains?(interval, version) ⇒ Boolean
- #to_s ⇒ Object
- #unbounded? ⇒ Boolean
- #union(other) ⇒ Object
- #with_scheme(value) ⇒ Object
Constructor Details
#initialize(intervals = [], raw_constraints: nil, scheme: nil, exclusions: []) ⇒ VersionRange
Returns a new instance of VersionRange.
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# File 'lib/vers/version_range.rb', line 12 def initialize(intervals = [], raw_constraints: nil, scheme: nil, exclusions: []) canonical_scheme = Scheme.canonical(scheme) interval_schemes = intervals.compact.map(&:scheme).compact.map { |value| Scheme.canonical(value) }.uniq if interval_schemes.length > 1 || (canonical_scheme && interval_schemes.any? { |value| value != canonical_scheme }) raise ArgumentError, "Cannot combine different version range schemes" end @scheme = canonical_scheme || interval_schemes.first @intervals = intervals.select { |interval| interval && !interval.empty? }.map { |interval| interval.with_scheme(@scheme) } if @scheme @intervals.sort! { |a, b| compare_interval_bounds(a, b) } else @intervals.sort_by! { |i| [i.min || '', i.max || ''] } end @raw_constraints = raw_constraints @exclusions = exclusions.dup merge_overlapping_intervals! end |
Instance Attribute Details
#exclusions ⇒ Object (readonly)
Returns the value of attribute exclusions.
10 11 12 |
# File 'lib/vers/version_range.rb', line 10 def exclusions @exclusions end |
#intervals ⇒ Object (readonly)
Returns the value of attribute intervals.
10 11 12 |
# File 'lib/vers/version_range.rb', line 10 def intervals @intervals end |
#raw_constraints ⇒ Object (readonly)
Returns the value of attribute raw_constraints.
10 11 12 |
# File 'lib/vers/version_range.rb', line 10 def raw_constraints @raw_constraints end |
#scheme ⇒ Object (readonly)
Returns the value of attribute scheme.
10 11 12 |
# File 'lib/vers/version_range.rb', line 10 def scheme @scheme end |
Class Method Details
.empty(scheme: nil) ⇒ Object
31 32 33 |
# File 'lib/vers/version_range.rb', line 31 def self.empty(scheme: nil) new([], scheme: scheme) end |
.exact(version, scheme: nil) ⇒ Object
39 40 41 |
# File 'lib/vers/version_range.rb', line 39 def self.exact(version, scheme: nil) new([Interval.exact(version, scheme: scheme)], scheme: scheme) end |
.greater_than(version, inclusive: false, scheme: nil) ⇒ Object
43 44 45 |
# File 'lib/vers/version_range.rb', line 43 def self.greater_than(version, inclusive: false, scheme: nil) new([Interval.greater_than(version, inclusive: inclusive, scheme: scheme)], scheme: scheme) end |
Instance Method Details
#compatible_scheme(other) ⇒ Object
239 240 241 242 243 244 245 |
# File 'lib/vers/version_range.rb', line 239 def compatible_scheme(other) if scheme && other.scheme && scheme != other.scheme raise ArgumentError, "Cannot combine #{scheme} and #{other.scheme} version ranges" end scheme || other.scheme end |
#complement ⇒ Object
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/vers/version_range.rb', line 247 def complement unless exclusions.empty? base = self.class.new(intervals, scheme: scheme).complement exclusions.each do |excluded| base = base.union(self.class.exact(excluded, scheme: scheme)) end return base end return self.class.unbounded(scheme: @scheme) if empty? return self.class.empty(scheme: @scheme) if unbounded? result_intervals = [] sorted_intervals = if @scheme intervals.sort { |a, b| compare_interval_bounds(a, b) } else intervals.sort_by { |i| i.min || '' } end first_interval = sorted_intervals.first if first_interval.min result_intervals << Interval.new( max: first_interval.min, max_inclusive: !first_interval.min_inclusive, scheme: @scheme ) end sorted_intervals.each_cons(2) do |curr, next_interval| if curr.max && next_interval.min comparison = version_compare(curr.max, next_interval.min) if comparison < 0 || (comparison == 0 && (!curr.max_inclusive || !next_interval.min_inclusive)) result_intervals << Interval.new( min: curr.max, max: next_interval.min, min_inclusive: !curr.max_inclusive, max_inclusive: !next_interval.min_inclusive, scheme: @scheme ) end end end last_interval = sorted_intervals.last if last_interval.max result_intervals << Interval.new( min: last_interval.max, min_inclusive: !last_interval.max_inclusive, scheme: @scheme ) end self.class.new(result_intervals, scheme: @scheme) end |
#composer_interval_contains?(interval, version) ⇒ Boolean
77 78 79 80 81 82 83 84 85 86 |
# File 'lib/vers/version_range.rb', line 77 def composer_interval_contains?(interval, version) candidate_branch = ComposerVersion.branch?(version) minimum_branch = interval.min && ComposerVersion.branch?(interval.min) maximum_branch = interval.max && ComposerVersion.branch?(interval.max) return interval.contains?(version) unless candidate_branch || minimum_branch || maximum_branch return true if interval.unbounded? candidate_branch && minimum_branch && maximum_branch && interval.min_inclusive && interval.max_inclusive && interval.min == interval.max && version == interval.min end |
#contains?(version) ⇒ Boolean
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/vers/version_range.rb', line 59 def contains?(version) if VALIDATED_CONTAINMENT_SCHEMES.include?(scheme) && !Version.valid?(version, scheme) return false end return false if exclusions.any? { |excluded| excluded_version?(version, excluded) } intervals.any? do |interval| contains = if scheme == "composer" composer_interval_contains?(interval, version) elsif scheme == "pypi" pypi_interval_contains?(interval, version) else interval.contains?(version) end contains && prerelease_allowed?(interval, version) end end |
#empty? ⇒ Boolean
51 52 53 |
# File 'lib/vers/version_range.rb', line 51 def empty? intervals.empty? end |
#exact_interval?(interval) ⇒ Boolean
202 203 204 205 |
# File 'lib/vers/version_range.rb', line 202 def exact_interval?(interval) interval.min && interval.max && interval.min_inclusive && interval.max_inclusive && Version.compare_for_range(interval.min, interval.max, interval.scheme || scheme).zero? end |
#exclude(version) ⇒ Object
303 304 305 306 307 308 309 310 311 312 |
# File 'lib/vers/version_range.rb', line 303 def exclude(version) return self unless contains?(version) self.class.new( intervals, raw_constraints: raw_constraints, scheme: scheme, exclusions: exclusions + [version] ) end |
#excluded_version?(version, excluded) ⇒ Boolean
146 147 148 149 150 151 152 153 154 |
# File 'lib/vers/version_range.rb', line 146 def excluded_version?(version, excluded) if scheme == "pypi" PyPIVersion.specifier_equal?(version, excluded) elsif scheme == "composer" && (ComposerVersion.branch?(version) || ComposerVersion.branch?(excluded)) version == excluded else Version.compare_with_scheme(version, excluded, scheme).zero? end end |
#intersect(other) ⇒ Object
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/vers/version_range.rb', line 156 def intersect(other) merged_scheme = compatible_scheme(other) result_intervals = [] intervals.each do |interval1| other.intervals.each do |interval2| intersection = intersect_intervals(interval1, interval2, merged_scheme) result_intervals << intersection unless intersection.empty? end end combined_raw = (raw_constraints || intervals) + (other.raw_constraints || other.intervals) self.class.new( result_intervals, raw_constraints: combined_raw, scheme: merged_scheme, exclusions: exclusions + other.exclusions ) end |
#intersect_intervals(left, right, comparison_scheme) ⇒ Object
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'lib/vers/version_range.rb', line 176 def intersect_intervals(left, right, comparison_scheme) return left.intersect(right) unless comparison_scheme == "pypi" left = left.with_scheme(comparison_scheme) right = right.with_scheme(comparison_scheme) left_exact = exact_interval?(left) right_exact = exact_interval?(right) return left.intersect(right) unless left_exact || right_exact if left_exact && right_exact left_contains_right = pypi_interval_contains?(left, right.min) right_contains_left = pypi_interval_contains?(right, left.min) return right if left_contains_right && !right_contains_left return left if right_contains_left return left if left_contains_right return Interval.empty(scheme: comparison_scheme) end exact = left_exact ? left : right other_interval = left_exact ? right : left return exact if pypi_interval_contains?(other_interval, exact.min) Interval.empty(scheme: comparison_scheme) end |
#prerelease_allowed?(interval, version) ⇒ Boolean
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/vers/version_range.rb', line 129 def prerelease_allowed?(interval, version) return true unless %w[npm cargo].include?(scheme) candidate = SemverVersion.parse(version.to_s.strip) return false unless candidate return true if candidate.prerelease.empty? [interval.min, interval.max].compact.any? do |bound| parsed_bound = SemverVersion.parse(bound.to_s.strip) next false unless parsed_bound && !parsed_bound.prerelease.empty? candidate.core.zip(parsed_bound.core).all? do |candidate_part, bound_part| VersionComparison.compare_numbers(candidate_part, bound_part).zero? end end end |
#pypi_interval_contains?(interval, version) ⇒ Boolean
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/vers/version_range.rb', line 88 def pypi_interval_contains?(interval, version) if interval.min && interval.max && interval.min_inclusive && interval.max_inclusive && PyPIVersion.compare(interval.min, interval.max).zero? return PyPIVersion.specifier_equal?(version, interval.min) end return false unless interval.contains?(version) candidate = PyPIVersion.parse(version) return false unless candidate if interval.min && !interval.min_inclusive bound = PyPIVersion.parse(interval.min) if bound return false if PyPIVersion.specifier_equal?(version, interval.min) without_post = PyPIVersion.without_post_and_dev(candidate) if candidate.post_number && PyPIVersion.versions_equal?(without_post, bound, ignore_local: true) return false end end end if interval.max && !interval.max_inclusive bound = PyPIVersion.parse(interval.max) if bound bound_is_release = bound.pre_tag.nil? && bound.post_number.nil? && bound.dev_number.nil? if bound_is_release && PyPIVersion.same_release?(candidate, bound) && (!candidate.pre_tag.nil? || !candidate.dev_number.nil?) return false end without_dev = PyPIVersion.without_dev(candidate) if candidate.dev_number && PyPIVersion.versions_equal?(without_dev, bound, ignore_local: true) return false end end end true end |
#to_s ⇒ Object
314 315 316 317 |
# File 'lib/vers/version_range.rb', line 314 def to_s return "∅" if empty? return intervals.map(&:to_s).join(" ∪ ") end |
#unbounded? ⇒ Boolean
55 56 57 |
# File 'lib/vers/version_range.rb', line 55 def unbounded? exclusions.empty? && intervals.length == 1 && intervals.first.unbounded? end |
#union(other) ⇒ Object
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/vers/version_range.rb', line 207 def union(other) merged_scheme = compatible_scheme(other) combined_raw = (raw_constraints || intervals) + (other.raw_constraints || other.intervals) left = with_scheme(merged_scheme) right = other.with_scheme(merged_scheme) combined_exclusions = [] left.exclusions.each do |excluded| combined_exclusions << excluded unless right.contains?(excluded) end right.exclusions.each do |excluded| next if left.contains?(excluded) || combined_exclusions.include?(excluded) combined_exclusions << excluded end self.class.new( intervals + other.intervals, raw_constraints: combined_raw, scheme: merged_scheme, exclusions: combined_exclusions ) end |
#with_scheme(value) ⇒ Object
229 230 231 232 233 234 235 236 237 |
# File 'lib/vers/version_range.rb', line 229 def with_scheme(value) canonical = Scheme.canonical(value) if scheme && canonical && scheme != canonical raise ArgumentError, "Cannot combine #{scheme} and #{canonical} version ranges" end return self if scheme == canonical self.class.new(intervals, raw_constraints: raw_constraints, scheme: canonical, exclusions: exclusions) end |