versionian 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +8 -0
  4. data/CODE_OF_CONDUCT.md +132 -0
  5. data/README.adoc +874 -0
  6. data/Rakefile +12 -0
  7. data/docs/Gemfile +8 -0
  8. data/docs/_guides/custom-schemes.adoc +110 -0
  9. data/docs/_guides/index.adoc +12 -0
  10. data/docs/_pages/component-types.adoc +151 -0
  11. data/docs/_pages/declarative-schemes.adoc +260 -0
  12. data/docs/_pages/index.adoc +15 -0
  13. data/docs/_pages/range-matching.adoc +102 -0
  14. data/docs/_pages/schemes.adoc +68 -0
  15. data/docs/_references/api.adoc +251 -0
  16. data/docs/_references/index.adoc +13 -0
  17. data/docs/_references/schemes.adoc +410 -0
  18. data/docs/_tutorials/getting-started.adoc +119 -0
  19. data/docs/_tutorials/index.adoc +11 -0
  20. data/docs/index.adoc +287 -0
  21. data/lib/versionian/component_definition.rb +71 -0
  22. data/lib/versionian/component_types/base.rb +19 -0
  23. data/lib/versionian/component_types/date_part.rb +41 -0
  24. data/lib/versionian/component_types/enum.rb +32 -0
  25. data/lib/versionian/component_types/float.rb +23 -0
  26. data/lib/versionian/component_types/hash.rb +21 -0
  27. data/lib/versionian/component_types/integer.rb +23 -0
  28. data/lib/versionian/component_types/postfix.rb +51 -0
  29. data/lib/versionian/component_types/prerelease.rb +70 -0
  30. data/lib/versionian/component_types/registry.rb +35 -0
  31. data/lib/versionian/component_types/string.rb +21 -0
  32. data/lib/versionian/errors/invalid_scheme_error.rb +7 -0
  33. data/lib/versionian/errors/invalid_version_error.rb +7 -0
  34. data/lib/versionian/errors/parse_error.rb +7 -0
  35. data/lib/versionian/parsers/declarative.rb +214 -0
  36. data/lib/versionian/scheme_loader.rb +102 -0
  37. data/lib/versionian/scheme_registry.rb +34 -0
  38. data/lib/versionian/schemes/calver.rb +94 -0
  39. data/lib/versionian/schemes/composite.rb +82 -0
  40. data/lib/versionian/schemes/declarative.rb +138 -0
  41. data/lib/versionian/schemes/pattern.rb +236 -0
  42. data/lib/versionian/schemes/semantic.rb +136 -0
  43. data/lib/versionian/schemes/solover.rb +121 -0
  44. data/lib/versionian/schemes/wendtver.rb +141 -0
  45. data/lib/versionian/version.rb +6 -0
  46. data/lib/versionian/version_component.rb +28 -0
  47. data/lib/versionian/version_identifier.rb +121 -0
  48. data/lib/versionian/version_range.rb +61 -0
  49. data/lib/versionian/version_scheme.rb +68 -0
  50. data/lib/versionian.rb +64 -0
  51. data/lib/versius.rb +5 -0
  52. data/sig/versius.rbs +4 -0
  53. metadata +157 -0
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ module Schemes
5
+ class Pattern < VersionScheme
6
+ attr_reader :pattern, :pattern_regex
7
+
8
+ # Format template uses placeholders like {major}, {minor}, etc.
9
+ # Example: "{major}.{minor}.{patch}" or "{year}.{month}-{build}"
10
+
11
+ def initialize(name:, pattern:, component_definitions:, format_template: nil, description: nil)
12
+ # Process component_definitions first (convert hashes to ComponentDefinition objects)
13
+ processed_definitions = component_definitions.map do |d|
14
+ d.is_a?(Hash) ? ComponentDefinition.from_hash(d) : ComponentDefinition.new(**d)
15
+ end
16
+ validate_component_definitions!(processed_definitions)
17
+
18
+ # Derive or use provided format_template
19
+ template = format_template || derive_format_template(pattern)
20
+
21
+ # Call super with all parameters
22
+ super(
23
+ name: name,
24
+ description: description,
25
+ format_template: template,
26
+ component_definitions: processed_definitions
27
+ )
28
+
29
+ @pattern = pattern
30
+ @pattern_regex = compile_and_validate_pattern(pattern)
31
+ end
32
+
33
+ def validate_component_definitions!(definitions = @component_definitions)
34
+ raise Errors::InvalidSchemeError, "No component definitions provided" if definitions.empty?
35
+ end
36
+
37
+ def parse(version_string)
38
+ validate_version_string(version_string)
39
+
40
+ match = version_string.match(@pattern_regex)
41
+ raise Errors::ParseError, "Version '#{version_string}' does not match pattern #{@pattern}" unless match
42
+
43
+ components = extract_components(match)
44
+ validate_component_count(match.captures.length)
45
+
46
+ comparable_array = build_comparable_array(components)
47
+
48
+ VersionIdentifier.new(
49
+ raw_string: version_string,
50
+ scheme: self,
51
+ components: components,
52
+ comparable_array: comparable_array
53
+ )
54
+ end
55
+
56
+ def compare_arrays(a, b)
57
+ # Lexicographic comparison
58
+ max_len = [a.length, b.length].max
59
+ max_len.times do |i|
60
+ a_val = a[i]
61
+ b_val = b[i]
62
+
63
+ # Handle nil values (optional components)
64
+ a_val = 0 if a_val.nil?
65
+ b_val = 0 if b_val.nil?
66
+
67
+ cmp = a_val <=> b_val
68
+ return cmp if cmp != 0
69
+ end
70
+
71
+ a.length <=> b.length
72
+ end
73
+
74
+ def matches_range?(version_string, range)
75
+ version = parse(version_string)
76
+ comparable = version.comparable_array
77
+
78
+ case range.type
79
+ when :equals
80
+ comparable == parse(range.boundary).comparable_array
81
+ when :before
82
+ comparable < parse(range.boundary).comparable_array
83
+ when :after
84
+ comparable >= parse(range.boundary).comparable_array
85
+ when :between
86
+ comparable >= parse(range.from).comparable_array &&
87
+ comparable <= parse(range.to).comparable_array
88
+ end
89
+ end
90
+
91
+ def render(version)
92
+ # If no format template, return raw_string
93
+ return version.raw_string unless @format_template
94
+
95
+ # Reconstruct version from components using format template
96
+ # Format template supports optional segments with [] syntax
97
+ # Example: "{major}.{minor}.{patch}[.{patchlevel}]"
98
+ # "{major}.{minor}.{patch}[-{stage}-{iteration}]"
99
+ result = @format_template.dup
100
+
101
+ # First, handle optional segments marked with []
102
+ # Process from innermost to outermost to handle nested optionals
103
+ loop do
104
+ # Find innermost optional segment
105
+ match = result.match(/\[([^\[\]]+)\]/)
106
+ break unless match
107
+
108
+ optional_segment = match[1]
109
+ placeholder = match[0]
110
+
111
+ # Check if any component placeholders in this segment have values
112
+ has_value = false
113
+ optional_segment.scan(/\{(\w+)\}/) do |component_name|
114
+ component = version.component(component_name.first.to_sym)
115
+ has_value = true if component
116
+ end
117
+
118
+ # Replace with content if has value, empty string otherwise
119
+ replacement = has_value ? optional_segment : ""
120
+ result = result.sub(placeholder, replacement)
121
+ end
122
+
123
+ # Then replace component values for all placeholders
124
+ @component_definitions.each do |comp_def|
125
+ component = version.component(comp_def.name)
126
+ type = ComponentTypes.resolve(comp_def.type)
127
+
128
+ # Build placeholder string, converting symbol name to string
129
+ placeholder = "{#{comp_def.name.to_s}}"
130
+
131
+ if component
132
+ formatted_value = type.format(component.value)
133
+ result = result.gsub(placeholder, formatted_value)
134
+ else
135
+ # Remove empty placeholder
136
+ result = result.gsub(placeholder, "")
137
+ end
138
+ end
139
+
140
+ # puts "After component replacement: #{result.inspect}"
141
+
142
+ # Clean up any remaining empty optional segment markers
143
+ # e.g., "[-{stage}-{iteration}]" where both stage and iteration are nil becomes "[-]"
144
+ result = result.gsub(/\[-\]/, "").gsub(/\[-/, "-").gsub(/-\]/, "-")
145
+
146
+ # Clean up any double separators or leading/trailing separators
147
+ result = result.gsub(/\.+/, ".").gsub(/^-/, "").gsub(/-$/, "").gsub(/^\.+/, "").gsub(/\.+$/, "")
148
+ end
149
+
150
+ private
151
+
152
+ def compile_and_validate_pattern(pattern_str)
153
+ # Basic ReDoS protection
154
+ # Check for dangerous patterns that can cause catastrophic backtracking
155
+ if pattern_str.include?("\\d+*") || pattern_str.include?('\d+*')
156
+ raise Errors::InvalidSchemeError, "Pattern may cause catastrophic backtracking"
157
+ end
158
+
159
+ if pattern_str =~ /\(\([^(]*\*\)[^)]*\*\)/ || pattern_str =~ /\(\([^(]*\+\)[^)]*\+\)/
160
+ raise Errors::InvalidSchemeError, "Pattern may cause catastrophic backtracking"
161
+ end
162
+
163
+ raise Errors::InvalidSchemeError, "Pattern has too many nested quantifiers" if pattern_str.scan(/\{/).length > 5
164
+
165
+ Regexp.new(pattern_str)
166
+ end
167
+
168
+ def derive_format_template(pattern_str)
169
+ # Derive format template from pattern by replacing capture groups with placeholders
170
+ # Pattern: ^(\d+)\.(\d+)\.(\d+)$ -> Format: {name1}.{name2}.{name3}
171
+
172
+ # Find all capture groups and their positions
173
+ pattern_str.gsub(/^\^|\$$/, "") # Remove anchors
174
+
175
+ # Replace each capture group with a placeholder
176
+ # We need to track which capture group corresponds to which component
177
+ # Since @component_definitions might not be set yet, we defer to a simpler approach
178
+
179
+ # For now, just store the pattern and use raw_string for rendering
180
+ # Users can provide explicit format_template if they want custom rendering
181
+ nil
182
+ end
183
+
184
+ def extract_components(match)
185
+ @component_definitions.each_with_index.map do |comp_def, index|
186
+ raw_value = match[index + 1] # Skip full match
187
+
188
+ # Handle optional components
189
+ if raw_value.nil?
190
+ next nil if comp_def.optional
191
+
192
+ # Skip optional components that didn't match
193
+
194
+ raise Errors::ParseError, "Required component '#{comp_def.name}' is missing"
195
+
196
+ end
197
+
198
+ type = ComponentTypes.resolve(comp_def.type)
199
+ parsed_value = type.parse(raw_value, comp_def)
200
+
201
+ VersionComponent.new(
202
+ name: comp_def.name,
203
+ type: comp_def.type,
204
+ value: parsed_value,
205
+ weight: comp_def.weight || 1,
206
+ values: comp_def.values,
207
+ order: comp_def.order || [],
208
+ definition: comp_def
209
+ )
210
+ end.compact # Remove nils from optional components
211
+ end
212
+
213
+ def build_comparable_array(components)
214
+ components.map do |component|
215
+ next if component.definition&.ignore_in_comparison
216
+
217
+ type = ComponentTypes.resolve(component.type)
218
+ type.to_comparable(component.value, component)
219
+ end.compact
220
+ end
221
+
222
+ def validate_component_count(capture_count)
223
+ # For patterns with optional components, we validate during extraction
224
+ # The capture_count here is total groups, but optional ones may be nil
225
+ # This is validated in extract_components where we check required components
226
+ end
227
+
228
+ def validate_version_string(version_string)
229
+ return unless version_string.nil? || version_string.strip.empty?
230
+
231
+ raise Errors::InvalidVersionError,
232
+ "Version string cannot be empty"
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems/version"
4
+
5
+ module Versionian
6
+ module Schemes
7
+ # Autoload all scheme classes
8
+ autoload :Pattern, "versionian/schemes/pattern"
9
+ autoload :Declarative, "versionian/schemes/declarative"
10
+ autoload :CalVer, "versionian/schemes/calver"
11
+ autoload :Composite, "versionian/schemes/composite"
12
+ autoload :SoloVer, "versionian/schemes/solover"
13
+ autoload :WendtVer, "versionian/schemes/wendtver"
14
+
15
+ class Semantic < VersionScheme
16
+ # SemVer pattern: MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]
17
+ # Must be exactly X.Y or X.Y.Z format where X, Y, Z are non-negative integers
18
+ # CalVer formats like YYYY.MM.DD should not match (year is 4 digits)
19
+ SEMVER_PATTERN = /\A\d{1,5}\.\d{1,5}(?:\.\d{1,5})?(?:-[\w.]+)?(?:\+[\w.]+)?\z/
20
+
21
+ def initialize(name: :semantic, description: "Semantic versioning (semver.org)")
22
+ component_definitions = [
23
+ { name: :major, type: :integer },
24
+ { name: :minor, type: :integer },
25
+ { name: :patch, type: :integer, optional: true }
26
+ ].map { |d| d.is_a?(Hash) ? ComponentDefinition.from_hash(d) : ComponentDefinition.new(**d) }
27
+
28
+ super(
29
+ name: name,
30
+ description: description,
31
+ component_definitions: component_definitions
32
+ )
33
+ end
34
+
35
+ def parse(version_string)
36
+ validate_version_string(version_string)
37
+
38
+ # Strip build metadata for Gem::Version compatibility
39
+ version_without_build = version_string.gsub(/\+.*$/, "")
40
+ gem_version = ::Gem::Version.new(version_without_build)
41
+ components = build_components(version_string, gem_version)
42
+ comparable_array = [gem_version] # Gem::Version is directly comparable
43
+
44
+ VersionIdentifier.new(
45
+ raw_string: version_string,
46
+ scheme: self,
47
+ components: components,
48
+ comparable_array: comparable_array
49
+ )
50
+ rescue ArgumentError => e
51
+ raise Errors::InvalidVersionError, "Invalid semantic version '#{version_string}': #{e.message}"
52
+ end
53
+
54
+ def compare_arrays(a, b)
55
+ # Gem::Version handles comparison
56
+ a <=> b
57
+ end
58
+
59
+ def matches_range?(version_string, range)
60
+ version = parse(version_string)
61
+ gem_version = version.comparable_array.first
62
+
63
+ case range.type
64
+ when :equals
65
+ gem_version == ::Gem::Version.new(range.boundary.gsub(/\+.*$/, ""))
66
+ when :before
67
+ gem_version < ::Gem::Version.new(range.boundary.gsub(/\+.*$/, ""))
68
+ when :after
69
+ gem_version >= ::Gem::Version.new(range.boundary.gsub(/\+.*$/, ""))
70
+ when :between
71
+ gem_version >= ::Gem::Version.new(range.from.gsub(/\+.*$/, "")) &&
72
+ gem_version <= ::Gem::Version.new(range.to.gsub(/\+.*$/, ""))
73
+ end
74
+ end
75
+
76
+ def valid?(version_string)
77
+ return false if version_string.nil? || version_string.strip.empty?
78
+ return false unless version_string =~ SEMVER_PATTERN
79
+
80
+ # Check if this looks like a CalVer date (first component is a 4-digit year)
81
+ # Exclude versions like 2024.01.15 that look like dates
82
+ first_component = version_string.split(/[-.+]/).first
83
+ if first_component =~ /^\d{4}$/ && first_component.to_i.between?(1900, 2100)
84
+ # This looks like a year, not a semantic version
85
+ return false
86
+ end
87
+
88
+ # Try to parse it
89
+ super
90
+ end
91
+
92
+ private
93
+
94
+ def build_components(version_string, gem_version)
95
+ # Extract components from version string
96
+ parts = version_string.split(/[+-]/).first.split(".")
97
+ components = []
98
+
99
+ components << VersionComponent.new(
100
+ name: :major,
101
+ type: :integer,
102
+ value: gem_version.segments[0] || 0,
103
+ weight: 1,
104
+ definition: @component_definitions[0]
105
+ )
106
+
107
+ components << VersionComponent.new(
108
+ name: :minor,
109
+ type: :integer,
110
+ value: gem_version.segments[1] || 0,
111
+ weight: 1,
112
+ definition: @component_definitions[1]
113
+ )
114
+
115
+ if gem_version.segments.length >= 3
116
+ components << VersionComponent.new(
117
+ name: :patch,
118
+ type: :integer,
119
+ value: gem_version.segments[2],
120
+ weight: 1,
121
+ definition: @component_definitions[2]
122
+ )
123
+ end
124
+
125
+ components
126
+ end
127
+
128
+ def validate_version_string(version_string)
129
+ return unless version_string.nil? || version_string.strip.empty?
130
+
131
+ raise Errors::InvalidVersionError,
132
+ "Version string cannot be empty"
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ module Schemes
5
+ class SoloVer < VersionScheme
6
+ SOLO_PATTERN = /^(\d+)(([+-])([a-zA-Z0-9]+))?$/
7
+
8
+ def initialize(name: :solover, description: "SoloVer single number with optional postfix")
9
+ super
10
+ end
11
+
12
+ def parse(version_string)
13
+ validate_version_string(version_string)
14
+
15
+ match = version_string.match(SOLO_PATTERN)
16
+ raise Errors::ParseError, "Invalid SoloVer format '#{version_string}'" unless match
17
+
18
+ number = match[1].to_i
19
+ postfix = match[2]
20
+ prefix = match[3]
21
+ identifier = match[4]
22
+
23
+ components = [
24
+ VersionComponent.new(
25
+ name: :number,
26
+ type: :integer,
27
+ value: number,
28
+ weight: 1
29
+ )
30
+ ]
31
+
32
+ comparable_array = [number]
33
+
34
+ if postfix
35
+ components << VersionComponent.new(
36
+ name: :postfix,
37
+ type: :postfix,
38
+ value: { prefix: prefix, identifier: identifier },
39
+ weight: 1
40
+ )
41
+
42
+ # Store prefix order and identifier for comparison
43
+ prefix_order = case prefix
44
+ when "+" then 1
45
+ when "-" then 2
46
+ else 0
47
+ end
48
+ comparable_array << prefix_order
49
+ comparable_array << identifier
50
+ end
51
+
52
+ VersionIdentifier.new(
53
+ raw_string: version_string,
54
+ scheme: self,
55
+ components: components,
56
+ comparable_array: comparable_array
57
+ )
58
+ end
59
+
60
+ def compare_arrays(a, b)
61
+ # Compare number first
62
+ number_cmp = a[0] <=> b[0]
63
+ return number_cmp if number_cmp != 0
64
+
65
+ # Check if both have postfixes or both don't
66
+ a_has_postfix = a.length > 1
67
+ b_has_postfix = b.length > 1
68
+
69
+ if !a_has_postfix && !b_has_postfix
70
+ return 0
71
+ elsif !a_has_postfix
72
+ return -1 # No postfix < +postfix
73
+ elsif !b_has_postfix
74
+ return 1
75
+ end
76
+
77
+ # Both have postfixes, compare by prefix
78
+ prefix_cmp = a[1] <=> b[1]
79
+ return prefix_cmp if prefix_cmp != 0
80
+
81
+ # Same prefix, compare identifier lexicographically
82
+ a[2] <=> b[2]
83
+ end
84
+
85
+ def matches_range?(version_string, range)
86
+ version_a = parse(version_string)
87
+ version_b = parse(range.boundary) if range.boundary
88
+ version_from = parse(range.from) if range.from
89
+ version_to = parse(range.to) if range.to
90
+
91
+ case range.type
92
+ when :equals
93
+ compare_arrays(version_a.comparable_array, version_b.comparable_array).zero?
94
+ when :before
95
+ compare_arrays(version_a.comparable_array, version_b.comparable_array).negative?
96
+ when :after
97
+ compare_arrays(version_a.comparable_array, version_b.comparable_array) >= 0
98
+ when :between
99
+ compare_arrays(version_a.comparable_array, version_from.comparable_array) >= 0 &&
100
+ compare_arrays(version_a.comparable_array, version_to.comparable_array) <= 0
101
+ end
102
+ end
103
+
104
+ def render(version)
105
+ postfix_component = version.component(:postfix)
106
+ if postfix_component&.value
107
+ "#{version.component(:number).value}#{postfix_component.value[:prefix]}#{postfix_component.value[:identifier]}"
108
+ else
109
+ version.component(:number).value.to_s
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def validate_version_string(version_string)
116
+ raise Errors::InvalidVersionError, "Version string cannot be nil" if version_string.nil?
117
+ raise Errors::InvalidVersionError, "Version string cannot be empty" if version_string.strip.empty?
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ module Schemes
5
+ class WendtVer < VersionScheme
6
+ WENDT_PATTERN = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
7
+
8
+ # WendtVer uses auto-incrementing components with carryover
9
+ # Format: major.minor.patch.build
10
+ # When build reaches 999, it resets to 0 and increments patch
11
+ # When patch reaches 99, it resets to 0 and increments minor
12
+ # When minor reaches 99, it resets to 0 and increments major
13
+
14
+ MAX_VALUES = { build: 999, patch: 99, minor: 99, major: Float::INFINITY }.freeze
15
+
16
+ def initialize(name: :wendtver, description: "WendtVer auto-incrementing with carryover")
17
+ super
18
+ end
19
+
20
+ def parse(version_string)
21
+ validate_version_string(version_string)
22
+
23
+ match = version_string.match(WENDT_PATTERN)
24
+ raise Errors::ParseError, "Invalid WendtVer format '#{version_string}'" unless match
25
+
26
+ major = match[1].to_i
27
+ minor = match[2].to_i
28
+ patch = match[3].to_i
29
+ build = match[4].to_i
30
+
31
+ validate_ranges(major, minor, patch, build)
32
+
33
+ components = [
34
+ VersionComponent.new(name: :major, type: :integer, value: major, weight: 1),
35
+ VersionComponent.new(name: :minor, type: :integer, value: minor, weight: 1),
36
+ VersionComponent.new(name: :patch, type: :integer, value: patch, weight: 1),
37
+ VersionComponent.new(name: :build, type: :integer, value: build, weight: 1)
38
+ ]
39
+
40
+ comparable_array = [major, minor, patch, build]
41
+
42
+ VersionIdentifier.new(
43
+ raw_string: version_string,
44
+ scheme: self,
45
+ components: components,
46
+ comparable_array: comparable_array
47
+ )
48
+ end
49
+
50
+ def compare_arrays(a, b)
51
+ # Standard lexicographic comparison
52
+ a <=> b
53
+ end
54
+
55
+ def matches_range?(version_string, range)
56
+ version_a = parse(version_string)
57
+ version_b = parse(range.boundary) if range.boundary
58
+ version_from = parse(range.from) if range.from
59
+ version_to = parse(range.to) if range.to
60
+
61
+ case range.type
62
+ when :equals
63
+ compare_arrays(version_a.comparable_array, version_b.comparable_array).zero?
64
+ when :before
65
+ compare_arrays(version_a.comparable_array, version_b.comparable_array).negative?
66
+ when :after
67
+ compare_arrays(version_a.comparable_array, version_b.comparable_array) >= 0
68
+ when :between
69
+ compare_arrays(version_a.comparable_array, version_from.comparable_array) >= 0 &&
70
+ compare_arrays(version_a.comparable_array, version_to.comparable_array) <= 0
71
+ end
72
+ end
73
+
74
+ def render(version)
75
+ [version.component(:major).value,
76
+ version.component(:minor).value,
77
+ version.component(:patch).value,
78
+ version.component(:build).value].join(".")
79
+ end
80
+
81
+ # Increment a version by one position
82
+ def increment(version_string, position = :build)
83
+ version = parse(version_string)
84
+ major = version.component(:major).value
85
+ minor = version.component(:minor).value
86
+ patch = version.component(:patch).value
87
+ build = version.component(:build).value
88
+
89
+ case position
90
+ when :build
91
+ build += 1
92
+ if build > MAX_VALUES[:build]
93
+ build = 0
94
+ patch += 1
95
+ if patch > MAX_VALUES[:patch]
96
+ patch = 0
97
+ minor += 1
98
+ if minor > MAX_VALUES[:minor]
99
+ minor = 0
100
+ major += 1
101
+ end
102
+ end
103
+ end
104
+ when :patch
105
+ patch += 1
106
+ if patch > MAX_VALUES[:patch]
107
+ patch = 0
108
+ minor += 1
109
+ if minor > MAX_VALUES[:minor]
110
+ minor = 0
111
+ major += 1
112
+ end
113
+ end
114
+ when :minor
115
+ minor += 1
116
+ if minor > MAX_VALUES[:minor]
117
+ minor = 0
118
+ major += 1
119
+ end
120
+ when :major
121
+ major += 1
122
+ end
123
+
124
+ "#{major}.#{minor}.#{patch}.#{build}"
125
+ end
126
+
127
+ private
128
+
129
+ def validate_version_string(version_string)
130
+ raise Errors::InvalidVersionError, "Version string cannot be nil" if version_string.nil?
131
+ raise Errors::InvalidVersionError, "Version string cannot be empty" if version_string.strip.empty?
132
+ end
133
+
134
+ def validate_ranges(_major, minor, patch, build)
135
+ raise Errors::ParseError, "Minor must be 0-99" unless minor.between?(0, 99)
136
+ raise Errors::ParseError, "Patch must be 0-99" unless patch.between?(0, 99)
137
+ raise Errors::ParseError, "Build must be 0-999" unless build.between?(0, 999)
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ VERSION_NUMBER = "0.1.0"
5
+ VERSION = VERSION_NUMBER
6
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ class VersionComponent
5
+ attr_reader :name, :type, :value, :weight, :values, :order, :definition
6
+
7
+ def initialize(name:, type:, value:, weight: 1, values: [], order: [], definition: nil)
8
+ @name = name
9
+ @type = type
10
+ @value = value
11
+ @weight = weight
12
+ @values = values
13
+ @order = order
14
+ @definition = definition
15
+ freeze
16
+ end
17
+
18
+ def to_comparable
19
+ type_class = ComponentTypes.resolve(@type)
20
+ type_class.to_comparable(@value, self)
21
+ end
22
+
23
+ def to_s
24
+ type_class = ComponentTypes.resolve(@type)
25
+ type_class.format(@value)
26
+ end
27
+ end
28
+ end