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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/README.adoc +874 -0
- data/Rakefile +12 -0
- data/docs/Gemfile +8 -0
- data/docs/_guides/custom-schemes.adoc +110 -0
- data/docs/_guides/index.adoc +12 -0
- data/docs/_pages/component-types.adoc +151 -0
- data/docs/_pages/declarative-schemes.adoc +260 -0
- data/docs/_pages/index.adoc +15 -0
- data/docs/_pages/range-matching.adoc +102 -0
- data/docs/_pages/schemes.adoc +68 -0
- data/docs/_references/api.adoc +251 -0
- data/docs/_references/index.adoc +13 -0
- data/docs/_references/schemes.adoc +410 -0
- data/docs/_tutorials/getting-started.adoc +119 -0
- data/docs/_tutorials/index.adoc +11 -0
- data/docs/index.adoc +287 -0
- data/lib/versionian/component_definition.rb +71 -0
- data/lib/versionian/component_types/base.rb +19 -0
- data/lib/versionian/component_types/date_part.rb +41 -0
- data/lib/versionian/component_types/enum.rb +32 -0
- data/lib/versionian/component_types/float.rb +23 -0
- data/lib/versionian/component_types/hash.rb +21 -0
- data/lib/versionian/component_types/integer.rb +23 -0
- data/lib/versionian/component_types/postfix.rb +51 -0
- data/lib/versionian/component_types/prerelease.rb +70 -0
- data/lib/versionian/component_types/registry.rb +35 -0
- data/lib/versionian/component_types/string.rb +21 -0
- data/lib/versionian/errors/invalid_scheme_error.rb +7 -0
- data/lib/versionian/errors/invalid_version_error.rb +7 -0
- data/lib/versionian/errors/parse_error.rb +7 -0
- data/lib/versionian/parsers/declarative.rb +214 -0
- data/lib/versionian/scheme_loader.rb +102 -0
- data/lib/versionian/scheme_registry.rb +34 -0
- data/lib/versionian/schemes/calver.rb +94 -0
- data/lib/versionian/schemes/composite.rb +82 -0
- data/lib/versionian/schemes/declarative.rb +138 -0
- data/lib/versionian/schemes/pattern.rb +236 -0
- data/lib/versionian/schemes/semantic.rb +136 -0
- data/lib/versionian/schemes/solover.rb +121 -0
- data/lib/versionian/schemes/wendtver.rb +141 -0
- data/lib/versionian/version.rb +6 -0
- data/lib/versionian/version_component.rb +28 -0
- data/lib/versionian/version_identifier.rb +121 -0
- data/lib/versionian/version_range.rb +61 -0
- data/lib/versionian/version_scheme.rb +68 -0
- data/lib/versionian.rb +64 -0
- data/lib/versius.rb +5 -0
- data/sig/versius.rbs +4 -0
- 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
|