semantic_puppet 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +38 -0
- data/README.md +64 -0
- data/Rakefile +69 -0
- data/lib/semantic_puppet.rb +7 -0
- data/lib/semantic_puppet/dependency.rb +181 -0
- data/lib/semantic_puppet/dependency/graph.rb +60 -0
- data/lib/semantic_puppet/dependency/graph_node.rb +117 -0
- data/lib/semantic_puppet/dependency/module_release.rb +46 -0
- data/lib/semantic_puppet/dependency/source.rb +25 -0
- data/lib/semantic_puppet/dependency/unsatisfiable_graph.rb +31 -0
- data/lib/semantic_puppet/version.rb +185 -0
- data/lib/semantic_puppet/version_range.rb +422 -0
- data/semantic_puppet.gemspec +30 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/unit/semantic_puppet/dependency/graph_node_spec.rb +141 -0
- data/spec/unit/semantic_puppet/dependency/graph_spec.rb +162 -0
- data/spec/unit/semantic_puppet/dependency/module_release_spec.rb +143 -0
- data/spec/unit/semantic_puppet/dependency/source_spec.rb +5 -0
- data/spec/unit/semantic_puppet/dependency/unsatisfiable_graph_spec.rb +44 -0
- data/spec/unit/semantic_puppet/dependency_spec.rb +383 -0
- data/spec/unit/semantic_puppet/version_range_spec.rb +307 -0
- data/spec/unit/semantic_puppet/version_spec.rb +644 -0
- metadata +169 -0
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'semantic_puppet/dependency'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module SemanticPuppet
|
5
|
+
module Dependency
|
6
|
+
module GraphNode
|
7
|
+
include Comparable
|
8
|
+
|
9
|
+
def name
|
10
|
+
end
|
11
|
+
|
12
|
+
# Determines whether the modules dependencies are satisfied by the known
|
13
|
+
# releases.
|
14
|
+
#
|
15
|
+
# @return [Boolean] true if all dependencies are satisfied
|
16
|
+
def satisfied?
|
17
|
+
dependencies.none? { |_, v| v.empty? }
|
18
|
+
end
|
19
|
+
|
20
|
+
def children
|
21
|
+
@_children ||= {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def populate_children(nodes)
|
25
|
+
if children.empty?
|
26
|
+
nodes = nodes.select { |node| satisfies_dependency?(node) }
|
27
|
+
nodes.each do |node|
|
28
|
+
children[node.name] = node
|
29
|
+
node.populate_children(nodes)
|
30
|
+
end
|
31
|
+
self.freeze
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @api internal
|
36
|
+
# @return [{ String => SortedSet<GraphNode> }] the satisfactory
|
37
|
+
# dependency nodes
|
38
|
+
def dependencies
|
39
|
+
@_dependencies ||= Hash.new { |h, k| h[k] = SortedSet.new }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Adds the given dependency name to the list of dependencies.
|
43
|
+
#
|
44
|
+
# @param name [String] the dependency name
|
45
|
+
# @return [void]
|
46
|
+
def add_dependency(name)
|
47
|
+
dependencies[name]
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Array<String>] the list of dependency names
|
51
|
+
def dependency_names
|
52
|
+
dependencies.keys
|
53
|
+
end
|
54
|
+
|
55
|
+
def constraints
|
56
|
+
@_constraints ||= Hash.new { |h, k| h[k] = [] }
|
57
|
+
end
|
58
|
+
|
59
|
+
def constraints_for(name)
|
60
|
+
return [] unless constraints.has_key?(name)
|
61
|
+
|
62
|
+
constraints[name].map do |constraint|
|
63
|
+
{
|
64
|
+
:source => constraint[0],
|
65
|
+
:description => constraint[1],
|
66
|
+
:test => constraint[2],
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Constrains the named module to suitable releases, as determined by the
|
72
|
+
# given block.
|
73
|
+
#
|
74
|
+
# @example Version-locking currently installed modules
|
75
|
+
# installed_modules.each do |m|
|
76
|
+
# @graph.add_constraint('installed', m.name, m.version) do |node|
|
77
|
+
# m.version == node.version
|
78
|
+
# end
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# @param source [String, Symbol] a name describing the source of the
|
82
|
+
# constraint
|
83
|
+
# @param mod [String] the name of the module
|
84
|
+
# @param desc [String] a description of the enforced constraint
|
85
|
+
# @yieldparam node [GraphNode] the node to test the constraint against
|
86
|
+
# @yieldreturn [Boolean] whether the node passed the constraint
|
87
|
+
# @return [void]
|
88
|
+
def add_constraint(source, mod, desc, &block)
|
89
|
+
constraints["#{mod}"] << [ source, desc, block ]
|
90
|
+
end
|
91
|
+
|
92
|
+
def satisfies_dependency?(node)
|
93
|
+
dependencies.key?(node.name) && satisfies_constraints?(node)
|
94
|
+
end
|
95
|
+
|
96
|
+
# @param release [ModuleRelease] the release to test
|
97
|
+
def satisfies_constraints?(release)
|
98
|
+
constraints_for(release.name).all? { |x| x[:test].call(release) }
|
99
|
+
end
|
100
|
+
|
101
|
+
def << (nodes)
|
102
|
+
Array(nodes).each do |node|
|
103
|
+
next unless dependencies.key?(node.name)
|
104
|
+
if satisfies_dependency?(node)
|
105
|
+
dependencies[node.name] << node
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
return self
|
110
|
+
end
|
111
|
+
|
112
|
+
def <=>(other)
|
113
|
+
name <=> other.name
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'semantic_puppet/dependency'
|
2
|
+
|
3
|
+
module SemanticPuppet
|
4
|
+
module Dependency
|
5
|
+
class ModuleRelease
|
6
|
+
include GraphNode
|
7
|
+
|
8
|
+
attr_reader :name, :version
|
9
|
+
|
10
|
+
# Create a new instance of a module release.
|
11
|
+
#
|
12
|
+
# @param source [SemanticPuppet::Dependency::Source]
|
13
|
+
# @param name [String]
|
14
|
+
# @param version [SemanticPuppet::Version]
|
15
|
+
# @param dependencies [{String => SemanticPuppet::VersionRange}]
|
16
|
+
def initialize(source, name, version, dependencies = {})
|
17
|
+
@source = source
|
18
|
+
@name = name.freeze
|
19
|
+
@version = version.freeze
|
20
|
+
|
21
|
+
dependencies.each do |name, range|
|
22
|
+
add_constraint('initialize', name, range.to_s) do |node|
|
23
|
+
range === node.version
|
24
|
+
end
|
25
|
+
|
26
|
+
add_dependency(name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def priority
|
31
|
+
@source.priority
|
32
|
+
end
|
33
|
+
|
34
|
+
def <=>(oth)
|
35
|
+
our_key = [ priority, name, version ]
|
36
|
+
their_key = [ oth.priority, oth.name, oth.version ]
|
37
|
+
|
38
|
+
return our_key <=> their_key
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
"#<#{self.class} #{name}@#{version}>"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'semantic_puppet/dependency'
|
2
|
+
|
3
|
+
module SemanticPuppet
|
4
|
+
module Dependency
|
5
|
+
class Source
|
6
|
+
def self.priority
|
7
|
+
0
|
8
|
+
end
|
9
|
+
|
10
|
+
def priority
|
11
|
+
self.class.priority
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_release(name, version, dependencies = {})
|
15
|
+
version = Version.parse(version) if version.is_a? String
|
16
|
+
dependencies = dependencies.inject({}) do |hash, (key, value)|
|
17
|
+
hash[key] = VersionRange.parse(value || '>= 0.0.0')
|
18
|
+
hash[key] ||= VersionRange::EMPTY_RANGE
|
19
|
+
hash
|
20
|
+
end
|
21
|
+
ModuleRelease.new(self, name, version, dependencies)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'semantic_puppet/dependency'
|
2
|
+
|
3
|
+
module SemanticPuppet
|
4
|
+
module Dependency
|
5
|
+
class UnsatisfiableGraph < StandardError
|
6
|
+
attr_reader :graph
|
7
|
+
|
8
|
+
def initialize(graph)
|
9
|
+
@graph = graph
|
10
|
+
|
11
|
+
deps = sentence_from_list(graph.modules)
|
12
|
+
super "Could not find satisfying releases for #{deps}"
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def sentence_from_list(list)
|
18
|
+
case list.length
|
19
|
+
when 1
|
20
|
+
list.first
|
21
|
+
when 2
|
22
|
+
list.join(' and ')
|
23
|
+
else
|
24
|
+
list = list.dup
|
25
|
+
list.push("and #{list.pop}")
|
26
|
+
list.join(', ')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
require 'semantic_puppet'
|
2
|
+
|
3
|
+
module SemanticPuppet
|
4
|
+
|
5
|
+
# @note SemanticPuppet::Version subclasses Numeric so that it has sane Range
|
6
|
+
# semantics in Ruby 1.9+.
|
7
|
+
class Version < Numeric
|
8
|
+
include Comparable
|
9
|
+
|
10
|
+
class ValidationFailure < ArgumentError; end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
# Parse a Semantic Version string.
|
14
|
+
#
|
15
|
+
# @param ver [String] the version string to parse
|
16
|
+
# @return [Version] a comparable {Version} object
|
17
|
+
def parse(ver)
|
18
|
+
match, major, minor, patch, prerelease, build = *ver.match(/\A#{REGEX_FULL}\Z/)
|
19
|
+
|
20
|
+
if match.nil?
|
21
|
+
raise "Unable to parse '#{ver}' as a semantic version identifier"
|
22
|
+
end
|
23
|
+
|
24
|
+
prerelease = parse_prerelease(prerelease) if prerelease
|
25
|
+
# Build metadata is not yet supported in semantic_puppet, but we hope to.
|
26
|
+
# The following code prevents build metadata for now.
|
27
|
+
#build = parse_build_metadata(build) if build
|
28
|
+
if !build.nil?
|
29
|
+
raise "'#{ver}' MUST NOT include build identifiers"
|
30
|
+
end
|
31
|
+
|
32
|
+
self.new(major.to_i, minor.to_i, patch.to_i, prerelease, build)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Validate a Semantic Version string.
|
36
|
+
#
|
37
|
+
# @param ver [String] the version string to validate
|
38
|
+
# @return [bool] whether or not the string represents a valid Semantic Version
|
39
|
+
def valid?(ver)
|
40
|
+
!!(ver =~ /\A#{REGEX_FULL}\Z/)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def parse_prerelease(prerelease)
|
45
|
+
subject = 'Prerelease identifiers'
|
46
|
+
prerelease = prerelease.split('.', -1)
|
47
|
+
|
48
|
+
if prerelease.empty? or prerelease.any? { |x| x.empty? }
|
49
|
+
raise "#{subject} MUST NOT be empty"
|
50
|
+
elsif prerelease.any? { |x| x =~ /[^0-9a-zA-Z-]/ }
|
51
|
+
raise "#{subject} MUST use only ASCII alphanumerics and hyphens"
|
52
|
+
elsif prerelease.any? { |x| x =~ /^0\d+$/ }
|
53
|
+
raise "#{subject} MUST NOT contain leading zeroes"
|
54
|
+
end
|
55
|
+
|
56
|
+
return prerelease.map { |x| x =~ /^\d+$/ ? x.to_i : x }
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_build_metadata(build)
|
60
|
+
subject = 'Build identifiers'
|
61
|
+
build = build.split('.', -1)
|
62
|
+
|
63
|
+
if build.empty? or build.any? { |x| x.empty? }
|
64
|
+
raise "#{subject} MUST NOT be empty"
|
65
|
+
elsif build.any? { |x| x =~ /[^0-9a-zA-Z-]/ }
|
66
|
+
raise "#{subject} MUST use only ASCII alphanumerics and hyphens"
|
67
|
+
end
|
68
|
+
|
69
|
+
return build
|
70
|
+
end
|
71
|
+
|
72
|
+
def raise(msg)
|
73
|
+
super ValidationFailure, msg, caller.drop_while { |x| x !~ /\bparse\b/ }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
attr_reader :major, :minor, :patch
|
78
|
+
|
79
|
+
def initialize(major, minor, patch, prerelease = nil, build = nil)
|
80
|
+
@major = major
|
81
|
+
@minor = minor
|
82
|
+
@patch = patch
|
83
|
+
@prerelease = prerelease
|
84
|
+
@build = build
|
85
|
+
end
|
86
|
+
|
87
|
+
def next(part)
|
88
|
+
case part
|
89
|
+
when :major
|
90
|
+
self.class.new(@major.next, 0, 0)
|
91
|
+
when :minor
|
92
|
+
self.class.new(@major, @minor.next, 0)
|
93
|
+
when :patch
|
94
|
+
self.class.new(@major, @minor, @patch.next)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def prerelease
|
99
|
+
@prerelease && @prerelease.join('.')
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [Boolean] true if this is a stable release
|
103
|
+
def stable?
|
104
|
+
@prerelease.nil?
|
105
|
+
end
|
106
|
+
|
107
|
+
def build
|
108
|
+
@build && @build.join('.')
|
109
|
+
end
|
110
|
+
|
111
|
+
def <=>(other)
|
112
|
+
return self.major <=> other.major unless self.major == other.major
|
113
|
+
return self.minor <=> other.minor unless self.minor == other.minor
|
114
|
+
return self.patch <=> other.patch unless self.patch == other.patch
|
115
|
+
return compare_prerelease(other)
|
116
|
+
end
|
117
|
+
|
118
|
+
def to_s
|
119
|
+
"#{major}.#{minor}.#{patch}" +
|
120
|
+
(@prerelease.nil? || prerelease.empty? ? '' : "-" + prerelease) +
|
121
|
+
(@build.nil? || build.empty? ? '' : "+" + build )
|
122
|
+
end
|
123
|
+
|
124
|
+
def hash
|
125
|
+
self.to_s.hash
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
# This is a hack; tildes sort later than any valid identifier. The
|
130
|
+
# advantage is that we don't need to handle stable vs. prerelease
|
131
|
+
# comparisons separately.
|
132
|
+
@@STABLE_RELEASE = [ '~' ].freeze
|
133
|
+
|
134
|
+
def compare_prerelease(other)
|
135
|
+
all_mine = @prerelease || @@STABLE_RELEASE
|
136
|
+
all_yours = other.instance_variable_get(:@prerelease) || @@STABLE_RELEASE
|
137
|
+
|
138
|
+
# Precedence is determined by comparing each dot separated identifier from
|
139
|
+
# left to right...
|
140
|
+
size = [ all_mine.size, all_yours.size ].max
|
141
|
+
Array.new(size).zip(all_mine, all_yours) do |_, mine, yours|
|
142
|
+
|
143
|
+
# ...until a difference is found.
|
144
|
+
next if mine == yours
|
145
|
+
|
146
|
+
# Numbers are compared numerically, strings are compared ASCIIbetically.
|
147
|
+
if mine.class == yours.class
|
148
|
+
return mine <=> yours
|
149
|
+
|
150
|
+
# A larger set of pre-release fields has a higher precedence.
|
151
|
+
elsif mine.nil?
|
152
|
+
return -1
|
153
|
+
elsif yours.nil?
|
154
|
+
return 1
|
155
|
+
|
156
|
+
# Numeric identifiers always have lower precedence than non-numeric.
|
157
|
+
elsif mine.is_a? Numeric
|
158
|
+
return -1
|
159
|
+
elsif yours.is_a? Numeric
|
160
|
+
return 1
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
return 0
|
165
|
+
end
|
166
|
+
|
167
|
+
def first_prerelease
|
168
|
+
self.class.new(@major, @minor, @patch, [])
|
169
|
+
end
|
170
|
+
|
171
|
+
public
|
172
|
+
|
173
|
+
# Version string matching regexes
|
174
|
+
REGEX_NUMERIC = "(0|[1-9]\\d*)[.](0|[1-9]\\d*)[.](0|[1-9]\\d*)" # Major . Minor . Patch
|
175
|
+
REGEX_PRE = "(?:[-](.*?))?" # Prerelease
|
176
|
+
REGEX_BUILD = "(?:[+](.*?))?" # Build
|
177
|
+
REGEX_FULL = REGEX_NUMERIC + REGEX_PRE + REGEX_BUILD
|
178
|
+
|
179
|
+
# The lowest precedence Version possible
|
180
|
+
MIN = self.new(0, 0, 0, []).freeze
|
181
|
+
|
182
|
+
# The highest precedence Version possible
|
183
|
+
MAX = self.new((1.0/0.0), 0, 0).freeze
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,422 @@
|
|
1
|
+
require 'semantic_puppet'
|
2
|
+
|
3
|
+
module SemanticPuppet
|
4
|
+
class VersionRange < Range
|
5
|
+
class << self
|
6
|
+
# Parses a version range string into a comparable {VersionRange} instance.
|
7
|
+
#
|
8
|
+
# Currently parsed version range string may take any of the following:
|
9
|
+
# forms:
|
10
|
+
#
|
11
|
+
# * Regular Semantic Version strings
|
12
|
+
# * ex. `"1.0.0"`, `"1.2.3-pre"`
|
13
|
+
# * Partial Semantic Version strings
|
14
|
+
# * ex. `"1.0.x"`, `"1"`, `"2.X"`
|
15
|
+
# * Inequalities
|
16
|
+
# * ex. `"> 1.0.0"`, `"<3.2.0"`, `">=4.0.0"`
|
17
|
+
# * Approximate Versions
|
18
|
+
# * ex. `"~1.0.0"`, `"~ 3.2.0"`, `"~4.0.0"`
|
19
|
+
# * Inclusive Ranges
|
20
|
+
# * ex. `"1.0.0 - 1.3.9"`
|
21
|
+
# * Range Intersections
|
22
|
+
# * ex. `">1.0.0 <=2.3.0"`
|
23
|
+
#
|
24
|
+
# @param range_str [String] the version range string to parse
|
25
|
+
# @return [VersionRange] a new {VersionRange} instance
|
26
|
+
def parse(range_str)
|
27
|
+
partial = '\d+(?:[.]\d+)?(?:[.][x]|[.]\d+(?:[-][0-9a-z.-]*)?)?'
|
28
|
+
exact = '\d+[.]\d+[.]\d+(?:[-][0-9a-z.-]*)?'
|
29
|
+
|
30
|
+
range = range_str.gsub(/([(><=~])[ ]+/, '\1')
|
31
|
+
range = range.gsub(/ - /, '#').strip
|
32
|
+
|
33
|
+
return case range
|
34
|
+
when /\A(#{partial})\Z/i
|
35
|
+
parse_loose_version_expression($1)
|
36
|
+
when /\A([><][=]?)(#{exact})\Z/i
|
37
|
+
parse_inequality_expression($1, $2)
|
38
|
+
when /\A~(#{partial})\Z/i
|
39
|
+
parse_reasonably_close_expression($1)
|
40
|
+
when /\A(#{exact})#(#{exact})\Z/i
|
41
|
+
parse_inclusive_range_expression($1, $2)
|
42
|
+
when /[ ]+/
|
43
|
+
parse_intersection_expression(range)
|
44
|
+
else
|
45
|
+
raise ArgumentError
|
46
|
+
end
|
47
|
+
|
48
|
+
rescue ArgumentError
|
49
|
+
raise ArgumentError, "Unparsable version range: #{range_str.inspect}"
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Creates a new {VersionRange} from a range intersection expression.
|
55
|
+
#
|
56
|
+
# @param expr [String] a range intersection expression
|
57
|
+
# @return [VersionRange] a version range representing `expr`
|
58
|
+
def parse_intersection_expression(expr)
|
59
|
+
expr.split(/[ ]+/).map { |x| parse(x) }.inject { |a,b| a & b }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Creates a new {VersionRange} from a "loose" description of a Semantic
|
63
|
+
# Version number.
|
64
|
+
#
|
65
|
+
# @see .process_loose_expr
|
66
|
+
#
|
67
|
+
# @param expr [String] a "loose" version expression
|
68
|
+
# @return [VersionRange] a version range representing `expr`
|
69
|
+
def parse_loose_version_expression(expr)
|
70
|
+
start, finish = process_loose_expr(expr)
|
71
|
+
|
72
|
+
if start.stable?
|
73
|
+
start = start.send(:first_prerelease)
|
74
|
+
end
|
75
|
+
|
76
|
+
if finish.stable?
|
77
|
+
exclude = true
|
78
|
+
finish = finish.send(:first_prerelease)
|
79
|
+
end
|
80
|
+
|
81
|
+
self.new(start, finish, exclude)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Creates an open-ended version range from an inequality expression.
|
85
|
+
#
|
86
|
+
# @overload parse_inequality_expression('<', expr)
|
87
|
+
# {include:.parse_lt_expression}
|
88
|
+
#
|
89
|
+
# @overload parse_inequality_expression('<=', expr)
|
90
|
+
# {include:.parse_lte_expression}
|
91
|
+
#
|
92
|
+
# @overload parse_inequality_expression('>', expr)
|
93
|
+
# {include:.parse_gt_expression}
|
94
|
+
#
|
95
|
+
# @overload parse_inequality_expression('>=', expr)
|
96
|
+
# {include:.parse_gte_expression}
|
97
|
+
#
|
98
|
+
# @param comp ['<', '<=', '>', '>='] an inequality operator
|
99
|
+
# @param expr [String] a "loose" version expression
|
100
|
+
# @return [VersionRange] a range covering all versions in the inequality
|
101
|
+
def parse_inequality_expression(comp, expr)
|
102
|
+
case comp
|
103
|
+
when '>'
|
104
|
+
parse_gt_expression(expr)
|
105
|
+
when '>='
|
106
|
+
parse_gte_expression(expr)
|
107
|
+
when '<'
|
108
|
+
parse_lt_expression(expr)
|
109
|
+
when '<='
|
110
|
+
parse_lte_expression(expr)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns a range covering all versions greater than the given `expr`.
|
115
|
+
#
|
116
|
+
# @param expr [String] the version to be greater than
|
117
|
+
# @return [VersionRange] a range covering all versions greater than the
|
118
|
+
# given `expr`
|
119
|
+
def parse_gt_expression(expr)
|
120
|
+
if expr =~ /^[^+]*-/
|
121
|
+
start = Version.parse("#{expr}.0")
|
122
|
+
else
|
123
|
+
start = process_loose_expr(expr).last.send(:first_prerelease)
|
124
|
+
end
|
125
|
+
|
126
|
+
self.new(start, SemanticPuppet::Version::MAX)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns a range covering all versions greater than or equal to the given
|
130
|
+
# `expr`.
|
131
|
+
#
|
132
|
+
# @param expr [String] the version to be greater than or equal to
|
133
|
+
# @return [VersionRange] a range covering all versions greater than or
|
134
|
+
# equal to the given `expr`
|
135
|
+
def parse_gte_expression(expr)
|
136
|
+
if expr =~ /^[^+]*-/
|
137
|
+
start = Version.parse(expr)
|
138
|
+
else
|
139
|
+
start = process_loose_expr(expr).first.send(:first_prerelease)
|
140
|
+
end
|
141
|
+
|
142
|
+
self.new(start, SemanticPuppet::Version::MAX)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns a range covering all versions less than the given `expr`.
|
146
|
+
#
|
147
|
+
# @param expr [String] the version to be less than
|
148
|
+
# @return [VersionRange] a range covering all versions less than the
|
149
|
+
# given `expr`
|
150
|
+
def parse_lt_expression(expr)
|
151
|
+
if expr =~ /^[^+]*-/
|
152
|
+
finish = Version.parse(expr)
|
153
|
+
else
|
154
|
+
finish = process_loose_expr(expr).first.send(:first_prerelease)
|
155
|
+
end
|
156
|
+
|
157
|
+
self.new(SemanticPuppet::Version::MIN, finish, true)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Returns a range covering all versions less than or equal to the given
|
161
|
+
# `expr`.
|
162
|
+
#
|
163
|
+
# @param expr [String] the version to be less than or equal to
|
164
|
+
# @return [VersionRange] a range covering all versions less than or equal
|
165
|
+
# to the given `expr`
|
166
|
+
def parse_lte_expression(expr)
|
167
|
+
if expr =~ /^[^+]*-/
|
168
|
+
finish = Version.parse(expr)
|
169
|
+
self.new(SemanticPuppet::Version::MIN, finish)
|
170
|
+
else
|
171
|
+
finish = process_loose_expr(expr).last.send(:first_prerelease)
|
172
|
+
self.new(SemanticPuppet::Version::MIN, finish, true)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# The "reasonably close" expression is used to designate ranges that have
|
177
|
+
# a reasonable proximity to the given "loose" version number. These take
|
178
|
+
# the form:
|
179
|
+
#
|
180
|
+
# ~[Version]
|
181
|
+
#
|
182
|
+
# The general semantics of these expressions are that the given version
|
183
|
+
# forms a lower bound for the range, and the upper bound is either the
|
184
|
+
# next version number increment (at whatever precision the expression
|
185
|
+
# provides) or the next stable version (in the case of a prerelease
|
186
|
+
# version).
|
187
|
+
#
|
188
|
+
# @example "Reasonably close" major version
|
189
|
+
# "~1" # => (>=1.0.0 <2.0.0)
|
190
|
+
# @example "Reasonably close" minor version
|
191
|
+
# "~1.2" # => (>=1.2.0 <1.3.0)
|
192
|
+
# @example "Reasonably close" patch version
|
193
|
+
# "~1.2.3" # => (>=1.2.3 <1.3.0)
|
194
|
+
# @example "Reasonably close" prerelease version
|
195
|
+
# "~1.2.3-alpha" # => (>=1.2.3-alpha <1.2.4)
|
196
|
+
#
|
197
|
+
# @param expr [String] a "loose" expression to build the range around
|
198
|
+
# @return [VersionRange] a "reasonably close" version range
|
199
|
+
def parse_reasonably_close_expression(expr)
|
200
|
+
parsed, succ = process_loose_expr(expr)
|
201
|
+
|
202
|
+
if parsed.stable?
|
203
|
+
parsed = parsed.send(:first_prerelease)
|
204
|
+
|
205
|
+
# Handle the special case of "~1.2.3" expressions.
|
206
|
+
succ = succ.next(:minor) if ((parsed.major == succ.major) && (parsed.minor == succ.minor))
|
207
|
+
|
208
|
+
succ = succ.send(:first_prerelease)
|
209
|
+
self.new(parsed, succ, true)
|
210
|
+
else
|
211
|
+
self.new(parsed, succ.next(:patch).send(:first_prerelease), true)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# An "inclusive range" expression takes two version numbers (or partial
|
216
|
+
# version numbers) and creates a range that covers all versions between
|
217
|
+
# them. These take the form:
|
218
|
+
#
|
219
|
+
# [Version] - [Version]
|
220
|
+
#
|
221
|
+
# @param start [String] a "loose" expresssion for the start of the range
|
222
|
+
# @param finish [String] a "loose" expression for the end of the range
|
223
|
+
# @return [VersionRange] a {VersionRange} covering `start` to `finish`
|
224
|
+
def parse_inclusive_range_expression(start, finish)
|
225
|
+
start, _ = process_loose_expr(start)
|
226
|
+
_, finish = process_loose_expr(finish)
|
227
|
+
|
228
|
+
start = start.send(:first_prerelease) if start.stable?
|
229
|
+
if finish.stable?
|
230
|
+
exclude = true
|
231
|
+
finish = finish.send(:first_prerelease)
|
232
|
+
end
|
233
|
+
|
234
|
+
self.new(start, finish, exclude)
|
235
|
+
end
|
236
|
+
|
237
|
+
# A "loose expression" is one that takes the form of all or part of a
|
238
|
+
# valid Semantic Version number. Particularly:
|
239
|
+
#
|
240
|
+
# * [Major].[Minor].[Patch]-[Prerelease]
|
241
|
+
# * [Major].[Minor].[Patch]
|
242
|
+
# * [Major].[Minor]
|
243
|
+
# * [Major]
|
244
|
+
#
|
245
|
+
# Various placeholders are also permitted in "loose expressions"
|
246
|
+
# (typically an 'x' or an asterisk).
|
247
|
+
#
|
248
|
+
# This method parses these expressions into a minimal and maximal version
|
249
|
+
# number pair.
|
250
|
+
#
|
251
|
+
# @todo Stabilize whether the second value is inclusive or exclusive
|
252
|
+
#
|
253
|
+
# @param expr [String] a string containing a "loose" version expression
|
254
|
+
# @return [(VersionNumber, VersionNumber)] a minimal and maximal
|
255
|
+
# version pair for the given expression
|
256
|
+
def process_loose_expr(expr)
|
257
|
+
case expr
|
258
|
+
when /^(\d+)(?:[.][xX*])?$/
|
259
|
+
expr = "#{$1}.0.0"
|
260
|
+
arity = :major
|
261
|
+
when /^(\d+[.]\d+)(?:[.][xX*])?$/
|
262
|
+
expr = "#{$1}.0"
|
263
|
+
arity = :minor
|
264
|
+
when /^\d+[.]\d+[.]\d+$/
|
265
|
+
arity = :patch
|
266
|
+
end
|
267
|
+
|
268
|
+
version = next_version = Version.parse(expr)
|
269
|
+
|
270
|
+
if arity
|
271
|
+
next_version = version.next(arity)
|
272
|
+
end
|
273
|
+
|
274
|
+
[ version, next_version ]
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# Computes the intersection of a pair of ranges. If the ranges have no
|
279
|
+
# useful intersection, an empty range is returned.
|
280
|
+
#
|
281
|
+
# @param other [VersionRange] the range to intersect with
|
282
|
+
# @return [VersionRange] the common subset
|
283
|
+
def intersection(other)
|
284
|
+
raise NOT_A_VERSION_RANGE unless other.kind_of?(VersionRange)
|
285
|
+
|
286
|
+
if self.begin < other.begin
|
287
|
+
return other.intersection(self)
|
288
|
+
end
|
289
|
+
|
290
|
+
unless include?(other.begin) || other.include?(self.begin)
|
291
|
+
return EMPTY_RANGE
|
292
|
+
end
|
293
|
+
|
294
|
+
endpoint = ends_before?(other) ? self : other
|
295
|
+
VersionRange.new(self.begin, endpoint.end, endpoint.exclude_end?)
|
296
|
+
end
|
297
|
+
alias :& :intersection
|
298
|
+
|
299
|
+
# Returns a string representation of this range, prefering simple common
|
300
|
+
# expressions for comprehension.
|
301
|
+
#
|
302
|
+
# @return [String] a range expression representing this VersionRange
|
303
|
+
def to_s
|
304
|
+
start, finish = self.begin, self.end
|
305
|
+
inclusive = exclude_end? ? '' : '='
|
306
|
+
|
307
|
+
case
|
308
|
+
when EMPTY_RANGE == self
|
309
|
+
"<0.0.0"
|
310
|
+
when exact_version?, patch_version?
|
311
|
+
"#{ start }"
|
312
|
+
when minor_version?
|
313
|
+
"#{ start }".sub(/.0$/, '.x')
|
314
|
+
when major_version?
|
315
|
+
"#{ start }".sub(/.0.0$/, '.x')
|
316
|
+
when open_end? && start.to_s =~ /-.*[.]0$/
|
317
|
+
">#{ start }".sub(/.0$/, '')
|
318
|
+
when open_end?
|
319
|
+
">=#{ start }"
|
320
|
+
when open_begin?
|
321
|
+
"<#{ inclusive }#{ finish }"
|
322
|
+
else
|
323
|
+
">=#{ start } <#{ inclusive }#{ finish }"
|
324
|
+
end
|
325
|
+
end
|
326
|
+
alias :inspect :to_s
|
327
|
+
|
328
|
+
private
|
329
|
+
|
330
|
+
# Determines whether this {VersionRange} has an earlier endpoint than the
|
331
|
+
# give `other` range.
|
332
|
+
#
|
333
|
+
# @param other [VersionRange] the range to compare against
|
334
|
+
# @return [Boolean] true if the endpoint for this range is less than or
|
335
|
+
# equal to the endpoint of the `other` range.
|
336
|
+
def ends_before?(other)
|
337
|
+
self.end < other.end || (self.end == other.end && self.exclude_end?)
|
338
|
+
end
|
339
|
+
|
340
|
+
# Describes whether this range has an upper limit.
|
341
|
+
# @return [Boolean] true if this range has no upper limit
|
342
|
+
def open_end?
|
343
|
+
self.end == SemanticPuppet::Version::MAX
|
344
|
+
end
|
345
|
+
|
346
|
+
# Describes whether this range has a lower limit.
|
347
|
+
# @return [Boolean] true if this range has no lower limit
|
348
|
+
def open_begin?
|
349
|
+
self.begin == SemanticPuppet::Version::MIN
|
350
|
+
end
|
351
|
+
|
352
|
+
# Describes whether this range follows the patterns for matching all
|
353
|
+
# releases with the same exact version.
|
354
|
+
# @return [Boolean] true if this range matches only a single exact version
|
355
|
+
def exact_version?
|
356
|
+
self.begin == self.end
|
357
|
+
end
|
358
|
+
|
359
|
+
# Describes whether this range follows the patterns for matching all
|
360
|
+
# releases with the same major version.
|
361
|
+
# @return [Boolean] true if this range matches only a single major version
|
362
|
+
def major_version?
|
363
|
+
start, finish = self.begin, self.end
|
364
|
+
|
365
|
+
exclude_end? &&
|
366
|
+
start.major.next == finish.major &&
|
367
|
+
same_minor? && start.minor == 0 &&
|
368
|
+
same_patch? && start.patch == 0 &&
|
369
|
+
[start.prerelease, finish.prerelease] == ['', '']
|
370
|
+
end
|
371
|
+
|
372
|
+
# Describes whether this range follows the patterns for matching all
|
373
|
+
# releases with the same minor version.
|
374
|
+
# @return [Boolean] true if this range matches only a single minor version
|
375
|
+
def minor_version?
|
376
|
+
start, finish = self.begin, self.end
|
377
|
+
|
378
|
+
exclude_end? &&
|
379
|
+
same_major? &&
|
380
|
+
start.minor.next == finish.minor &&
|
381
|
+
same_patch? && start.patch == 0 &&
|
382
|
+
[start.prerelease, finish.prerelease] == ['', '']
|
383
|
+
end
|
384
|
+
|
385
|
+
# Describes whether this range follows the patterns for matching all
|
386
|
+
# releases with the same patch version.
|
387
|
+
# @return [Boolean] true if this range matches only a single patch version
|
388
|
+
def patch_version?
|
389
|
+
start, finish = self.begin, self.end
|
390
|
+
|
391
|
+
exclude_end? &&
|
392
|
+
same_major? &&
|
393
|
+
same_minor? &&
|
394
|
+
start.patch.next == finish.patch &&
|
395
|
+
[start.prerelease, finish.prerelease] == ['', '']
|
396
|
+
end
|
397
|
+
|
398
|
+
# @return [Boolean] true if `begin` and `end` share the same major verion
|
399
|
+
def same_major?
|
400
|
+
self.begin.major == self.end.major
|
401
|
+
end
|
402
|
+
|
403
|
+
# @return [Boolean] true if `begin` and `end` share the same minor verion
|
404
|
+
def same_minor?
|
405
|
+
self.begin.minor == self.end.minor
|
406
|
+
end
|
407
|
+
|
408
|
+
# @return [Boolean] true if `begin` and `end` share the same patch verion
|
409
|
+
def same_patch?
|
410
|
+
self.begin.patch == self.end.patch
|
411
|
+
end
|
412
|
+
|
413
|
+
undef :to_a
|
414
|
+
|
415
|
+
NOT_A_VERSION_RANGE = ArgumentError.new("value must be a #{VersionRange}")
|
416
|
+
|
417
|
+
public
|
418
|
+
|
419
|
+
# A range that matches no versions
|
420
|
+
EMPTY_RANGE = VersionRange.parse('< 0.0.0').freeze
|
421
|
+
end
|
422
|
+
end
|