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
data/docs/index.adoc ADDED
@@ -0,0 +1,287 @@
1
+ ---
2
+ layout: default
3
+ title: Overview
4
+ nav_order: 1
5
+ ---
6
+
7
+ = Versionian: Declarative versioning schemes
8
+
9
+ image:https://img.shields.io/gem/v/versionian.svg[RubyGems Version]
10
+ image:https://img.shields.io/github/license/lutaml/versionian.svg[License]
11
+ image:https://github.com/lutaml/versionian/actions/workflows/rake.yml/badge.svg["Build", link="https://github.com/lutaml/versionian/actions/workflows/rake.yml"]
12
+
13
+ == Purpose
14
+
15
+ Versionian is a Ruby library for declaring, parsing, comparing, and rendering version schemes.
16
+ It provides model-driven primitives for defining how versions work, supporting semantic versioning, calendar versioning, and unlimited custom schemes through declarative YAML configuration.
17
+
18
+ === Why Versionian?
19
+
20
+ Ruby's built-in `Gem::Version` handles semantic versioning well, but real-world projects use diverse versioning schemes that `Gem::Version` cannot parse or compare correctly:
21
+
22
+ * **Calendar versioning** (`2024.01.17`, `2024.03`) - `Gem::Version` parses these but cannot enforce date validation
23
+ * **Single-number versioning** (`5`, `5+hotfix`, `5-beta`) - `Gem::Version` treats postfixes incorrectly
24
+ * **Human-readable versions** (`Alpha.1.5`, `Beta.2.0`) - `Gem::Version` cannot compare these
25
+ * **Hash-based versions** (`2024.01.abc123`) - `Gem::Version` has no concept of commit hashes
26
+ * **Custom schemes** - Every project has unique versioning needs
27
+
28
+ === How Versionian is different
29
+
30
+ [width="100%",cols="3*m,4*m,3*m"]
31
+ |===
32
+ |Feature |Gem::Version |Versionian
33
+
34
+ |Supported schemes
35
+ |Semantic versioning only
36
+ |4 built-in schemes, unlimited custom
37
+
38
+ |Extensibility
39
+ |Not extensible
40
+ |Define custom schemes via declarative YAML or Ruby
41
+
42
+ |Component types
43
+ |Integers and dot-separated strings
44
+ |Integer, Float, Enum, DatePart, Hash, Postfix, Prerelease, String, Wildcard
45
+
46
+ |Comparison strategy
47
+ |Fixed SemVer rules
48
+ |Per-scheme comparison (lexicographic arrays)
49
+
50
+ |Validation
51
+ |Basic format validation
52
+ |Type-aware validation (date ranges, enum values, hash formats)
53
+
54
+ |Programmatic building
55
+ |Not supported
56
+ |Build versions from component values
57
+ |===
58
+
59
+ Versionian fills the gap between `Gem::Version`'s semantic versioning focus and the diverse versioning needs of real-world projects.
60
+
61
+ == Features
62
+
63
+ * link:core-topics/schemes.html[Built-in schemes] - SemVer, CalVer, SoloVer, WendtVer
64
+ * link:core-topics/declarative-schemes.html[Declarative schemes] - Custom via YAML
65
+ * link:core-topics/component-types.html[Component types] - Extensible type system
66
+ * link:core-topics/range-matching.html[Range matching] - Version range comparisons
67
+ * link:guides/custom-schemes.html[Custom Ruby schemes] - Ruby API for custom schemes
68
+ * link:references/api.html[API reference] - Complete API documentation
69
+
70
+ == Quick start
71
+
72
+ Get started with Versionian in three steps:
73
+
74
+ .Require the library
75
+ [source,ruby]
76
+ ----
77
+ require 'versionian'
78
+ ----
79
+
80
+ .Parse and compare versions
81
+ [source,ruby]
82
+ ----
83
+ # Use built-in semantic versioning
84
+ scheme = Versionian.get_scheme(:semantic)
85
+
86
+ version = scheme.parse("1.12.0")
87
+ puts version.to_s # => "1.12.0"
88
+
89
+ # Compare versions
90
+ result = scheme.compare("1.12.0", "2.0.0")
91
+ puts result # => -1 (less than)
92
+ ----
93
+
94
+ .Declare a custom scheme via YAML
95
+ [source,yaml]
96
+ ----
97
+ # schemes/my_scheme.yaml
98
+ name: my_scheme
99
+ type: declarative
100
+ description: Custom version scheme
101
+ components:
102
+ - name: major
103
+ type: integer
104
+ separator: "."
105
+ - name: minor
106
+ type: integer
107
+ separator: "."
108
+ - name: patch
109
+ type: integer
110
+ optional: true
111
+ ----
112
+
113
+ [source,ruby]
114
+ ----
115
+ # Load and use custom scheme
116
+ scheme = Versionian::SchemeLoader.from_yaml_file('schemes/my_scheme.yaml')
117
+ Versionian.register_scheme(:my_scheme, scheme)
118
+
119
+ version = scheme.parse("1.2.3")
120
+ puts version.major # => 1
121
+ ----
122
+
123
+ == Discovering available schemes
124
+
125
+ Versionian provides several ways to discover available schemes:
126
+
127
+ .List registered schemes
128
+ [source,ruby]
129
+ ----
130
+ # List all registered scheme names
131
+ scheme_names = Versionian.scheme_registry.registered
132
+ puts scheme_names.inspect # => [:semantic, :calver, :solover, :wendtver]
133
+
134
+ # Check if a scheme exists
135
+ Versionian.scheme_registry.registered.include?(:semantic) # => true
136
+ ----
137
+
138
+ .Detect scheme from version string
139
+ [source,ruby]
140
+ ----
141
+ # Auto-detect which scheme matches a version string
142
+ detected = Versionian.detect_scheme("1.2.3")
143
+ puts detected.name # => :semantic
144
+
145
+ detected = Versionian.detect_scheme("2024.01.17")
146
+ puts detected.name # => :calver
147
+
148
+ detected = Versionian.detect_scheme("5+hotfix")
149
+ puts detected.name # => :solover
150
+
151
+ # Returns nil for unrecognised version strings
152
+ detected = Versionian.detect_scheme("not-a-version")
153
+ puts detected # => nil
154
+ ----
155
+
156
+ .Get and use a scheme
157
+ [source,ruby]
158
+ ----
159
+ # Get a scheme by name
160
+ scheme = Versionian.get_scheme(:semantic)
161
+
162
+ # Check scheme capabilities
163
+ puts scheme.supports?("1.2.3") # => true
164
+ puts scheme.supports?("invalid") # => false
165
+
166
+ # Parse and compare
167
+ version = scheme.parse("1.12.0")
168
+ puts version.to_s # => "1.12.0"
169
+
170
+ # Raises error for unknown schemes
171
+ begin
172
+ Versionian.get_scheme(:unknown)
173
+ rescue Versionian::Errors::InvalidSchemeError => e
174
+ puts e.message # => "Unknown scheme: unknown"
175
+ end
176
+ ----
177
+
178
+ == Architecture
179
+
180
+ Versionian follows a model-driven architecture where version schemes define their own parsing, comparison, and rendering behavior.
181
+
182
+ === Lexicographic array comparison
183
+
184
+ Versionian uses lexicographic array comparison instead of weighted integer sums. Each version stores a `comparable_array` where each element represents a component in comparison order:
185
+
186
+ [source,ruby]
187
+ ----
188
+ # Version stores array for comparison
189
+ version.comparable_array # => [2024, 1, 17, :rc, 1]
190
+
191
+ # Comparison is element-by-element
192
+ [2024, 1, 17] <=> [2024, 1, 18] # => -1
193
+ [2024, 1, 17] <=> [2024, 2, 1] # => -1
194
+ [2024, 1, 17] <=> [2024, 1, 17] # => 0
195
+ ----
196
+
197
+ This approach avoids integer overflow, provides better performance, and allows each component type to define its own comparison semantics.
198
+
199
+ === Parse versus build
200
+
201
+ Versionian provides two ways to create version identifiers:
202
+
203
+ * **Parse**: Extract components from a version string
204
+ * **Build**: Create a version from component values
205
+
206
+ [source,ruby]
207
+ ----
208
+ # Parse: from string
209
+ version = scheme.parse("1.12.0")
210
+
211
+ # Build: from component values
212
+ version = scheme.build(major: 1, minor: 12, patch: 0)
213
+ ----
214
+
215
+ Both methods return the same `VersionIdentifier` object with a `comparable_array` for comparison.
216
+
217
+ === Object model
218
+
219
+ .Versionian object model
220
+ [source]
221
+ ----
222
+ Versionian::VersionScheme (abstract)
223
+ ├── parse(version_string) -> VersionIdentifier
224
+ ├── build(component_values) -> VersionIdentifier
225
+ ├── compare_arrays(a, b) -> Integer
226
+ ├── render(version) -> String
227
+ └── matches_range?(version_string, range) -> Boolean
228
+
229
+ Implemented Schemes:
230
+ ├── Semantic (SemVer)
231
+ ├── CalVer (Calendar versioning)
232
+ ├── SoloVer (Single number with postfix)
233
+ ├── WendtVer (Auto-incrementing)
234
+ └── Declarative (Custom segment-based)
235
+
236
+ VersionIdentifier objects:
237
+ ├── raw_string (original input)
238
+ ├── scheme (reference to scheme)
239
+ ├── components (array of VersionComponent)
240
+ └── comparable_array (for lexicographic comparison)
241
+
242
+ Component type registry:
243
+ ├── Integer (numeric sequences)
244
+ ├── Float (IEEE754 floats)
245
+ ├── String (arbitrary text)
246
+ ├── Enum (ordered stages)
247
+ ├── DatePart (calendar components)
248
+ ├── Prerelease (SemVer prereleases)
249
+ ├── Postfix (SoloVer suffixes)
250
+ ├── Hash (git commit hashes)
251
+ └── Custom types (user-defined)
252
+ ----
253
+
254
+ == Installation
255
+
256
+ Add this line to your application's Gemfile:
257
+
258
+ [source,ruby]
259
+ ----
260
+ gem 'versionian'
261
+ ----
262
+
263
+ And then execute:
264
+
265
+ [source,shell]
266
+ ----
267
+ bundle install
268
+ ----
269
+
270
+ Or install it yourself as:
271
+
272
+ [source,shell]
273
+ ----
274
+ gem install versionian
275
+ ----
276
+
277
+ == Contributing
278
+
279
+ . Fork the repository
280
+ . Create your feature branch (`git checkout -b my-new-feature`)
281
+ . Commit your changes (`git commit -am 'Add some feature'`)
282
+ . Push to the branch (`git push origin my-new-feature`)
283
+ . Create a new Pull Request
284
+
285
+ == License
286
+
287
+ MIT License - see LICENSE file for details.
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ class ComponentDefinition
5
+ attr_reader :name, :type, :subtype, :values, :order, :weight, :optional, :ignore_in_comparison, :default,
6
+ :compare_as, :separator, :prefix, :suffix, :min_count, :max_count, :validate, :include_prefix_in_value
7
+
8
+ def initialize(**attrs)
9
+ @name = attrs[:name]
10
+ @type = attrs[:type]
11
+ @subtype = attrs[:subtype]
12
+ @values = attrs[:values] || []
13
+ @order = attrs[:order] || []
14
+ @weight = attrs[:weight] || 1
15
+ @optional = attrs[:optional] || false
16
+ @ignore_in_comparison = attrs[:ignore_in_comparison] || false
17
+ @default = attrs[:default]
18
+ @compare_as = attrs[:compare_as]
19
+ @separator = attrs[:separator]
20
+ @prefix = attrs[:prefix]
21
+ @suffix = attrs[:suffix]
22
+ @min_count = attrs[:min_count] || 1
23
+ @max_count = attrs[:max_count] || 1
24
+ @validate = attrs[:validate] || {}
25
+ @include_prefix_in_value = attrs[:include_prefix_in_value] || false
26
+ freeze
27
+ end
28
+
29
+ def self.from_hash(hash)
30
+ new(
31
+ name: hash["name"]&.to_sym || hash[:name],
32
+ type: hash["type"]&.to_sym || hash[:type],
33
+ subtype: hash["subtype"] || hash[:subtype],
34
+ values: (hash["values"] || hash[:values] || []).map(&:to_sym),
35
+ order: (hash["order"] || hash[:order] || []).map(&:to_sym),
36
+ weight: hash["weight"] || hash[:weight],
37
+ optional: hash["optional"] || hash[:optional],
38
+ ignore_in_comparison: hash["ignore_in_comparison"] || hash[:ignore_in_comparison],
39
+ default: hash["default"] || hash[:default],
40
+ compare_as: hash["compare_as"] || hash[:compare_as],
41
+ separator: hash["separator"] || hash[:separator],
42
+ prefix: hash["prefix"] || hash[:prefix],
43
+ suffix: hash["suffix"] || hash[:suffix],
44
+ min_count: hash["min_count"] || hash[:min_count] || 1,
45
+ max_count: hash["max_count"] || hash[:max_count] || 1,
46
+ validate: hash["validate"] || hash[:validate] || {},
47
+ include_prefix_in_value: hash["include_prefix_in_value"] || hash[:include_prefix_in_value]
48
+ )
49
+ end
50
+
51
+ def validate!
52
+ raise Errors::InvalidSchemeError, "name is required" unless @name
53
+ raise Errors::InvalidSchemeError, "type is required" unless @type
54
+ end
55
+
56
+ # Check if this component definition defines a separator
57
+ def has_separator?
58
+ !@separator.nil? && !@separator.empty?
59
+ end
60
+
61
+ # Check if this component definition defines a prefix
62
+ def has_prefix?
63
+ !@prefix.nil? && !@prefix.empty?
64
+ end
65
+
66
+ # Check if this component definition defines a suffix
67
+ def has_suffix?
68
+ !@suffix.nil? && !@suffix.empty?
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ module ComponentTypes
5
+ class Base
6
+ def self.parse(value, definition)
7
+ raise NotImplementedError, "#{self} must implement .parse"
8
+ end
9
+
10
+ def self.to_comparable(value, definition)
11
+ raise NotImplementedError, "#{self} must implement .to_comparable"
12
+ end
13
+
14
+ def self.format(value)
15
+ raise NotImplementedError, "#{self} must implement .format"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ module ComponentTypes
5
+ class DatePart < Base
6
+ RANGES = {
7
+ year: [1, 9999],
8
+ month: [1, 12],
9
+ day: [1, 31],
10
+ week: [1, 53],
11
+ hour: [0, 23],
12
+ minute: [0, 59],
13
+ second: [0, 59]
14
+ }.freeze
15
+
16
+ def self.parse(value, definition)
17
+ return definition.default.to_i if value.nil? || value.empty?
18
+
19
+ int_val = Kernel.Integer(value)
20
+
21
+ subtype = definition.subtype
22
+ if subtype && RANGES.key?(subtype.to_sym)
23
+ min, max = RANGES[subtype.to_sym]
24
+ unless int_val.between?(min, max)
25
+ raise Errors::ParseError, "Invalid #{subtype} '#{int_val}'. Must be between #{min} and #{max}"
26
+ end
27
+ end
28
+
29
+ int_val
30
+ end
31
+
32
+ def self.to_comparable(value, _definition)
33
+ value
34
+ end
35
+
36
+ def self.format(value)
37
+ value.to_s.rjust(2, "0")
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ module ComponentTypes
5
+ class Enum < Base
6
+ def self.parse(value, definition)
7
+ return nil if value.nil? || value.empty?
8
+
9
+ sym = value.to_sym
10
+
11
+ if definition.values && !definition.values.empty? && !definition.values.include?(sym)
12
+ raise Errors::ParseError,
13
+ "Invalid enum value '#{value}' for #{definition.name}. Allowed: #{definition.values.join(", ")}"
14
+ end
15
+
16
+ sym
17
+ end
18
+
19
+ def self.to_comparable(value, definition)
20
+ return ::Float::INFINITY if value.nil?
21
+
22
+ # Use the definition's order array
23
+ order = definition.order || []
24
+ order.index(value) || (order.length + 1)
25
+ end
26
+
27
+ def self.format(value)
28
+ value.nil? ? "" : value.to_s
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ module ComponentTypes
5
+ class Float < Base
6
+ def self.parse(value, definition)
7
+ return definition.default.to_f if value.nil? || value.empty?
8
+
9
+ ::Kernel.Float(value)
10
+ rescue ArgumentError, TypeError => e
11
+ raise Errors::ParseError, "Invalid float '#{value}': #{e.message}"
12
+ end
13
+
14
+ def self.to_comparable(value, _definition)
15
+ value
16
+ end
17
+
18
+ def self.format(value)
19
+ value.to_s
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ module ComponentTypes
5
+ class Hash < Base
6
+ def self.parse(value, definition)
7
+ return definition.default if value.nil? || value.empty?
8
+
9
+ value.downcase # Normalize to lowercase
10
+ end
11
+
12
+ def self.to_comparable(value, _definition)
13
+ [value.length, value] # Compare by length first, then lexicographically
14
+ end
15
+
16
+ def self.format(value)
17
+ value
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ module ComponentTypes
5
+ class Integer < Base
6
+ def self.parse(value, definition)
7
+ return definition.default.to_i if value.nil? || value.empty?
8
+
9
+ Kernel.Integer(value)
10
+ rescue ArgumentError, TypeError => e
11
+ raise Errors::ParseError, "Invalid integer '#{value}': #{e.message}"
12
+ end
13
+
14
+ def self.to_comparable(value, _definition)
15
+ value
16
+ end
17
+
18
+ def self.format(value)
19
+ value.to_s
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ module ComponentTypes
5
+ class Postfix < Base
6
+ # Postfix format: [+|-]identifier
7
+ # + means "after" (hotfix)
8
+ # - means "before" (prerelease)
9
+ # No postfix is in the middle
10
+
11
+ def self.parse(value, _definition)
12
+ return nil if value.nil? || value.empty?
13
+
14
+ prefix = value[0]
15
+ identifier = value[1..]
16
+
17
+ { prefix: prefix, identifier: identifier }
18
+ end
19
+
20
+ def self.to_comparable(value, _definition)
21
+ return 0 if value.nil?
22
+
23
+ # Ordering: none (0) < + (1) < - (2)
24
+ case value[:prefix]
25
+ when "+" then 1
26
+ when "-" then 2
27
+ else 0
28
+ end
29
+ end
30
+
31
+ def self.format(value)
32
+ return "" if value.nil?
33
+
34
+ "#{value[:prefix]}#{value[:identifier]}"
35
+ end
36
+
37
+ def self.compare_postfixes(a, b)
38
+ return 0 if a.nil? && b.nil?
39
+ return -1 if a.nil? # No postfix < +postfix
40
+ return 1 if b.nil?
41
+
42
+ # Compare by prefix first
43
+ prefix_cmp = to_comparable(a) <=> to_comparable(b)
44
+ return prefix_cmp if prefix_cmp != 0
45
+
46
+ # Same prefix, compare identifier lexicographically
47
+ a[:identifier] <=> b[:identifier]
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ module ComponentTypes
5
+ class Prerelease < Base
6
+ def self.parse(value, _definition)
7
+ return nil if value.nil? || value.empty?
8
+
9
+ # Split by dots: "alpha.1" => ["alpha", "1"]
10
+ value.split(".").map do |part|
11
+ if part =~ /^\d+$/
12
+ part.to_i
13
+ else
14
+ part.to_sym
15
+ end
16
+ end
17
+ end
18
+
19
+ def self.to_comparable(value, _definition)
20
+ # Compare according to SemVer rules
21
+ # nil > any prerelease
22
+ return [1] if value.nil?
23
+
24
+ value
25
+ end
26
+
27
+ def self.format(value)
28
+ return "" if value.nil?
29
+
30
+ value.map(&:to_s).join(".")
31
+ end
32
+
33
+ # Custom comparison for SemVer prerelease rules
34
+ def self.compare_prerelease_arrays(a, b)
35
+ return 0 if a.nil? && b.nil?
36
+ return 1 if a.nil? # nil > any prerelease
37
+ return -1 if b.nil?
38
+
39
+ max_len = [a.length, b.length].max
40
+ max_len.times do |i|
41
+ a_val = a[i]
42
+ b_val = b[i]
43
+
44
+ # Missing identifier = lower priority
45
+ return -1 if a_val.nil?
46
+ return 1 if b_val.nil?
47
+
48
+ # Numeric < alphanumeric
49
+ a_is_num = a_val.is_a?(Integer)
50
+ b_is_num = b_val.is_a?(Integer)
51
+
52
+ if a_is_num && !b_is_num
53
+ return -1
54
+ elsif !a_is_num && b_is_num
55
+ return 1
56
+ elsif a_is_num && b_is_num
57
+ cmp = a_val <=> b_val
58
+ return cmp if cmp != 0
59
+ else
60
+ cmp = a_val.to_s <=> b_val.to_s
61
+ return cmp if cmp != 0
62
+ end
63
+ end
64
+
65
+ # All equal up to here, shorter is lower priority
66
+ a.length <=> b.length
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ module ComponentTypes
5
+ # Autoload component type classes
6
+ autoload :Base, "versionian/component_types/base"
7
+ autoload :Integer, "versionian/component_types/integer"
8
+ autoload :Float, "versionian/component_types/float"
9
+ autoload :Enum, "versionian/component_types/enum"
10
+ autoload :String, "versionian/component_types/string"
11
+ autoload :DatePart, "versionian/component_types/date_part"
12
+ autoload :Prerelease, "versionian/component_types/prerelease"
13
+ autoload :Postfix, "versionian/component_types/postfix"
14
+ autoload :Hash, "versionian/component_types/hash"
15
+
16
+ @types = {}
17
+ @mutex = Mutex.new
18
+
19
+ class << self
20
+ def register(name, type_class)
21
+ @mutex.synchronize do
22
+ @types[name] = type_class
23
+ end
24
+ end
25
+
26
+ def resolve(type)
27
+ @types[type] || raise(Errors::InvalidSchemeError, "Unknown component type: #{type}")
28
+ end
29
+
30
+ def registered
31
+ @types.keys
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Versionian
4
+ module ComponentTypes
5
+ class String < Base
6
+ def self.parse(value, definition)
7
+ return definition.default || "" if value.nil? || value.empty?
8
+
9
+ value.to_s
10
+ end
11
+
12
+ def self.to_comparable(value, _definition)
13
+ value
14
+ end
15
+
16
+ def self.format(value)
17
+ value
18
+ end
19
+ end
20
+ end
21
+ end