verbatim 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/CHANGELOG.md +28 -0
- data/LICENSE +21 -0
- data/README.md +133 -0
- data/lib/verbatim/cursor.rb +65 -0
- data/lib/verbatim/errors.rb +29 -0
- data/lib/verbatim/parser.rb +112 -0
- data/lib/verbatim/schema.rb +325 -0
- data/lib/verbatim/schemas/calver.rb +59 -0
- data/lib/verbatim/schemas/semver.rb +86 -0
- data/lib/verbatim/schemas/semver_compare.rb +85 -0
- data/lib/verbatim/segment.rb +56 -0
- data/lib/verbatim/types.rb +341 -0
- data/lib/verbatim/version.rb +9 -0
- data/lib/verbatim.rb +18 -0
- metadata +99 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Verbatim
|
|
4
|
+
# Registry and dispatch for segment type handlers (+:uint+, +:int+, custom types).
|
|
5
|
+
#
|
|
6
|
+
# @api public
|
|
7
|
+
#
|
|
8
|
+
module Types
|
|
9
|
+
class << self
|
|
10
|
+
# Parses a value from a string according to a segment type.
|
|
11
|
+
#
|
|
12
|
+
# @param type [Symbol]
|
|
13
|
+
# @param cursor [Cursor]
|
|
14
|
+
# @param segment [Segment]
|
|
15
|
+
# @param parse_ctx [Object] opaque context (currently {Parser}); reserved for handlers
|
|
16
|
+
# @return [Object] parsed value
|
|
17
|
+
# @raise [ArgumentError] if +type+ is unknown
|
|
18
|
+
# @raise [ParseError] if the handler rejects input
|
|
19
|
+
#
|
|
20
|
+
def parse(type, cursor, segment, parse_ctx)
|
|
21
|
+
handler = registry.fetch(type) { raise ArgumentError, "unknown segment type: #{type.inspect}" }
|
|
22
|
+
handler.parse(cursor, segment, parse_ctx)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Formats a value into a string according to a segment type.
|
|
26
|
+
#
|
|
27
|
+
# @param type [Symbol]
|
|
28
|
+
# @param value [Object]
|
|
29
|
+
# @param segment [Segment]
|
|
30
|
+
# @return [String] fragment for {#Schema#to_s}
|
|
31
|
+
# @raise [ArgumentError] if +type+ is unknown or +value+ is invalid for the type
|
|
32
|
+
#
|
|
33
|
+
def format(type, value, segment)
|
|
34
|
+
handler = registry.fetch(type) { raise ArgumentError, "unknown segment type: #{type.inspect}" }
|
|
35
|
+
handler.format(value, segment)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Registers a new type handler.
|
|
39
|
+
|
|
40
|
+
# @param type [Symbol]
|
|
41
|
+
# @param handler [Object] object responding to +parse(cursor, segment, parse_ctx)+ and +format(value, segment)+
|
|
42
|
+
# @return [void]
|
|
43
|
+
#
|
|
44
|
+
def register(type, handler)
|
|
45
|
+
registry[type] = handler
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Returns the registry of type handlers.
|
|
51
|
+
#
|
|
52
|
+
# @return [Hash{Symbol => Object}] registry of type handlers
|
|
53
|
+
#
|
|
54
|
+
def registry
|
|
55
|
+
@registry ||= {}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Internal helpers and built-in type handler implementations.
|
|
60
|
+
|
|
61
|
+
module Handlers
|
|
62
|
+
module_function
|
|
63
|
+
|
|
64
|
+
# Raises a parse error.
|
|
65
|
+
|
|
66
|
+
# @param cursor [Cursor]
|
|
67
|
+
# @param segment [Segment, nil]
|
|
68
|
+
# @param message [String]
|
|
69
|
+
# @return [void]
|
|
70
|
+
# @raise [ParseError]
|
|
71
|
+
|
|
72
|
+
def fail_parse!(cursor, segment, message)
|
|
73
|
+
raise ParseError.new(
|
|
74
|
+
message,
|
|
75
|
+
string: cursor.string,
|
|
76
|
+
index: cursor.pos,
|
|
77
|
+
segment: segment&.name
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Unsigned decimal integer segment type (+:uint+).
|
|
82
|
+
#
|
|
83
|
+
# @api public
|
|
84
|
+
#
|
|
85
|
+
class Uint
|
|
86
|
+
# Parses a value from a string according to a segment type.
|
|
87
|
+
#
|
|
88
|
+
# @param cursor [Cursor]
|
|
89
|
+
# @param segment [Segment]
|
|
90
|
+
# @param _parse_ctx [Object]
|
|
91
|
+
# @return [Integer]
|
|
92
|
+
# @raise [ParseError]
|
|
93
|
+
#
|
|
94
|
+
def parse(cursor, segment, _parse_ctx)
|
|
95
|
+
start = cursor.pos
|
|
96
|
+
Handlers.fail_parse!(cursor, segment, "expected digit") if cursor.eos? || cursor.peek !~ /\d/
|
|
97
|
+
|
|
98
|
+
cursor.advance(1) while !cursor.eos? && cursor.peek =~ /\d/
|
|
99
|
+
raw = cursor.string[start, cursor.pos - start]
|
|
100
|
+
if segment.options[:leading_zeros] == false && raw.length > 1 && raw.start_with?("0")
|
|
101
|
+
Handlers.fail_parse!(cursor, segment, "leading zeros not allowed")
|
|
102
|
+
end
|
|
103
|
+
value = Integer(raw, 10)
|
|
104
|
+
validate_uint_range!(cursor, segment, value)
|
|
105
|
+
|
|
106
|
+
value
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Formats a value into a string according to a segment type.
|
|
110
|
+
#
|
|
111
|
+
# @param value [Integer]
|
|
112
|
+
# @param segment [Segment]
|
|
113
|
+
# @return [String]
|
|
114
|
+
# @raise [ArgumentError]
|
|
115
|
+
#
|
|
116
|
+
def format(value, segment)
|
|
117
|
+
raise ArgumentError, "uint value must be Integer, got #{value.class}" unless value.is_a?(Integer)
|
|
118
|
+
|
|
119
|
+
pad = segment.options[:pad]
|
|
120
|
+
if pad
|
|
121
|
+
raise ArgumentError, "options[:pad] must be a positive Integer" unless pad.is_a?(Integer) && pad.positive?
|
|
122
|
+
|
|
123
|
+
Kernel.format("%0*d", pad, value)
|
|
124
|
+
else
|
|
125
|
+
value.to_s
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# Validates the range of a uint value.
|
|
132
|
+
#
|
|
133
|
+
# @param cursor [Cursor]
|
|
134
|
+
# @param segment [Segment]
|
|
135
|
+
# @param value [Integer]
|
|
136
|
+
# @return [void]
|
|
137
|
+
# @raise [ParseError]
|
|
138
|
+
#
|
|
139
|
+
def validate_uint_range!(cursor, segment, value)
|
|
140
|
+
if (min = segment.options[:minimum]) && value < min
|
|
141
|
+
Handlers.fail_parse!(cursor, segment, "value #{value} is below minimum #{min}")
|
|
142
|
+
end
|
|
143
|
+
return unless (max = segment.options[:maximum]) && value > max
|
|
144
|
+
|
|
145
|
+
Handlers.fail_parse!(cursor, segment, "value #{value} is above maximum #{max}")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Signed decimal integer (+optional leading +-+); +:int+ type.
|
|
150
|
+
#
|
|
151
|
+
# @api public
|
|
152
|
+
#
|
|
153
|
+
class Int
|
|
154
|
+
# Parses a value from a string according to a segment type.
|
|
155
|
+
#
|
|
156
|
+
# @param cursor [Cursor]
|
|
157
|
+
# @param segment [Segment]
|
|
158
|
+
# @param _parse_ctx [Object]
|
|
159
|
+
# @return [Integer]
|
|
160
|
+
# @raise [ParseError]
|
|
161
|
+
#
|
|
162
|
+
def parse(cursor, segment, _parse_ctx)
|
|
163
|
+
start = cursor.pos
|
|
164
|
+
cursor.advance(1) if !cursor.eos? && cursor.peek == "-"
|
|
165
|
+
Handlers.fail_parse!(cursor, segment, "expected digit") if cursor.eos? || cursor.peek !~ /\d/
|
|
166
|
+
|
|
167
|
+
cursor.advance(1) while !cursor.eos? && cursor.peek =~ /\d/
|
|
168
|
+
raw = cursor.string[start, cursor.pos - start]
|
|
169
|
+
Integer(raw, 10)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Formats a value into a string according to a segment type.
|
|
173
|
+
#
|
|
174
|
+
# @param value [Integer]
|
|
175
|
+
# @param _segment [Segment]
|
|
176
|
+
# @return [String]
|
|
177
|
+
# @raise [ArgumentError]
|
|
178
|
+
#
|
|
179
|
+
def format(value, _segment)
|
|
180
|
+
raise ArgumentError, "int value must be Integer, got #{value.class}" unless value.is_a?(Integer)
|
|
181
|
+
|
|
182
|
+
value.to_s
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Non-empty run of +[0-9A-Za-z-]+; +:token+ type.
|
|
187
|
+
#
|
|
188
|
+
# @api public
|
|
189
|
+
#
|
|
190
|
+
class Token
|
|
191
|
+
# Parses a value from a string according to a segment type.
|
|
192
|
+
#
|
|
193
|
+
# @param cursor [Cursor]
|
|
194
|
+
# @param segment [Segment]
|
|
195
|
+
# @param _parse_ctx [Object]
|
|
196
|
+
# @return [String]
|
|
197
|
+
# @raise [ParseError]
|
|
198
|
+
#
|
|
199
|
+
def parse(cursor, segment, _parse_ctx)
|
|
200
|
+
start = cursor.pos
|
|
201
|
+
if cursor.eos? || cursor.peek !~ /[0-9A-Za-z-]/
|
|
202
|
+
Handlers.fail_parse!(cursor, segment,
|
|
203
|
+
"expected token character")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
cursor.advance(1) while !cursor.eos? && cursor.peek =~ /[0-9A-Za-z-]/
|
|
207
|
+
cursor.string[start, cursor.pos - start]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Formats a value into a string according to a segment type.
|
|
211
|
+
#
|
|
212
|
+
# @param value [Object] coerced with +#to_s+
|
|
213
|
+
# @param _segment [Segment]
|
|
214
|
+
# @return [String]
|
|
215
|
+
# @raise [ArgumentError] if the string is empty
|
|
216
|
+
#
|
|
217
|
+
def format(value, _segment)
|
|
218
|
+
string = value.to_s
|
|
219
|
+
|
|
220
|
+
raise ArgumentError, "token must be non-empty" if string.empty?
|
|
221
|
+
|
|
222
|
+
string
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Match anchored regexp; +:string+ type (+options[:pattern]+ required).
|
|
227
|
+
#
|
|
228
|
+
# @api public
|
|
229
|
+
#
|
|
230
|
+
class StringType
|
|
231
|
+
# Parses a value from a string according to a segment type.
|
|
232
|
+
#
|
|
233
|
+
# @param cursor [Cursor]
|
|
234
|
+
# @param segment [Segment]
|
|
235
|
+
# @param _parse_ctx [Object]
|
|
236
|
+
# @return [String]
|
|
237
|
+
# @raise [ArgumentError] if +pattern+ is missing
|
|
238
|
+
# @raise [ParseError] if the remainder does not match
|
|
239
|
+
#
|
|
240
|
+
def parse(cursor, segment, _parse_ctx)
|
|
241
|
+
pattern = segment.options[:pattern]
|
|
242
|
+
raise ArgumentError, ":string requires options[:pattern] Regexp" unless pattern.is_a?(Regexp)
|
|
243
|
+
|
|
244
|
+
rest = cursor.remainder
|
|
245
|
+
anchored = Regexp.new("\\A(?:#{pattern.source})", pattern.options)
|
|
246
|
+
m = anchored.match(rest)
|
|
247
|
+
Handlers.fail_parse!(cursor, segment, "did not match pattern") unless m
|
|
248
|
+
|
|
249
|
+
cursor.advance(m.end(0))
|
|
250
|
+
m[0]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Formats a value into a string according to a segment type.
|
|
254
|
+
#
|
|
255
|
+
# @param value [Object]
|
|
256
|
+
# @param _segment [Segment]
|
|
257
|
+
# @return [String]
|
|
258
|
+
#
|
|
259
|
+
def format(value, _segment)
|
|
260
|
+
value.to_s
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Dot-separated SemVer identifier string; +:semver_ids+ type.
|
|
265
|
+
#
|
|
266
|
+
# @api public
|
|
267
|
+
#
|
|
268
|
+
class SemverIdentifiers
|
|
269
|
+
# Checks if a token is a valid SemVer 2.0 identifier.
|
|
270
|
+
#
|
|
271
|
+
# @param token [String]
|
|
272
|
+
# @return [Boolean] +true+ if +token+ is a valid SemVer 2.0 identifier
|
|
273
|
+
#
|
|
274
|
+
def self.valid_ident?(token)
|
|
275
|
+
return false if token.empty?
|
|
276
|
+
|
|
277
|
+
if token.match?(/\A[0-9]+\z/)
|
|
278
|
+
token == "0" || token.match?(/\A[1-9]\d*\z/)
|
|
279
|
+
else
|
|
280
|
+
token.match?(/\A[0-9A-Za-z-]+\z/)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Parses a value from a string according to a segment type.
|
|
285
|
+
#
|
|
286
|
+
# @param cursor [Cursor]
|
|
287
|
+
# @param segment [Segment]
|
|
288
|
+
# @param _parse_ctx [Object]
|
|
289
|
+
# @return [String] dot-separated identifiers (no leading +lead+)
|
|
290
|
+
# @raise [ParseError]
|
|
291
|
+
#
|
|
292
|
+
def parse(cursor, segment, _parse_ctx)
|
|
293
|
+
terminator = segment.options[:terminator]
|
|
294
|
+
rest = cursor.remainder
|
|
295
|
+
stop_idx = if terminator
|
|
296
|
+
idx = rest.index(terminator)
|
|
297
|
+
idx.nil? ? rest.length : idx
|
|
298
|
+
else
|
|
299
|
+
rest.length
|
|
300
|
+
end
|
|
301
|
+
chunk = rest[0, stop_idx]
|
|
302
|
+
Handlers.fail_parse!(cursor, segment, "expected semver identifiers") if chunk.empty?
|
|
303
|
+
|
|
304
|
+
chunk.split(".", -1).each do |p|
|
|
305
|
+
unless self.class.valid_ident?(p)
|
|
306
|
+
Handlers.fail_parse!(cursor, segment,
|
|
307
|
+
"invalid semver identifier #{p.inspect}")
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
cursor.advance(chunk.length)
|
|
311
|
+
|
|
312
|
+
chunk
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Formats a value into a string according to a segment type.
|
|
316
|
+
#
|
|
317
|
+
# @param value [Object]
|
|
318
|
+
# @param _segment [Segment]
|
|
319
|
+
# @return [String]
|
|
320
|
+
# @raise [ArgumentError] if empty or any identifier is invalid
|
|
321
|
+
#
|
|
322
|
+
def format(value, _segment)
|
|
323
|
+
string = value.to_s
|
|
324
|
+
raise ArgumentError, "semver identifiers must be non-empty" if string.empty?
|
|
325
|
+
|
|
326
|
+
string.split(".", -1).each do |p|
|
|
327
|
+
raise ArgumentError, "invalid semver identifier #{p.inspect}" unless self.class.valid_ident?(p)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
string
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
Verbatim::Types.register(:uint, Verbatim::Types::Handlers::Uint.new)
|
|
338
|
+
Verbatim::Types.register(:int, Verbatim::Types::Handlers::Int.new)
|
|
339
|
+
Verbatim::Types.register(:token, Verbatim::Types::Handlers::Token.new)
|
|
340
|
+
Verbatim::Types.register(:string, Verbatim::Types::Handlers::StringType.new)
|
|
341
|
+
Verbatim::Types.register(:semver_ids, Verbatim::Types::Handlers::SemverIdentifiers.new)
|
data/lib/verbatim.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "verbatim/version"
|
|
4
|
+
require_relative "verbatim/errors"
|
|
5
|
+
require_relative "verbatim/cursor"
|
|
6
|
+
require_relative "verbatim/segment"
|
|
7
|
+
require_relative "verbatim/types"
|
|
8
|
+
require_relative "verbatim/parser"
|
|
9
|
+
require_relative "verbatim/schema"
|
|
10
|
+
require_relative "verbatim/schemas/semver"
|
|
11
|
+
require_relative "verbatim/schemas/calver"
|
|
12
|
+
|
|
13
|
+
# Root namespace for the Verbatim version-schema library.
|
|
14
|
+
#
|
|
15
|
+
# @api public
|
|
16
|
+
#
|
|
17
|
+
module Verbatim
|
|
18
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: verbatim
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Dustin Dugal
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rake
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '13.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '13.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rspec
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.12'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.12'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rubocop
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.60'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.60'
|
|
54
|
+
description: 'Define version string layouts with a class DSL, parse with .parse, round-trip
|
|
55
|
+
with #to_s.'
|
|
56
|
+
executables: []
|
|
57
|
+
extensions: []
|
|
58
|
+
extra_rdoc_files: []
|
|
59
|
+
files:
|
|
60
|
+
- CHANGELOG.md
|
|
61
|
+
- LICENSE
|
|
62
|
+
- README.md
|
|
63
|
+
- lib/verbatim.rb
|
|
64
|
+
- lib/verbatim/cursor.rb
|
|
65
|
+
- lib/verbatim/errors.rb
|
|
66
|
+
- lib/verbatim/parser.rb
|
|
67
|
+
- lib/verbatim/schema.rb
|
|
68
|
+
- lib/verbatim/schemas/calver.rb
|
|
69
|
+
- lib/verbatim/schemas/semver.rb
|
|
70
|
+
- lib/verbatim/schemas/semver_compare.rb
|
|
71
|
+
- lib/verbatim/segment.rb
|
|
72
|
+
- lib/verbatim/types.rb
|
|
73
|
+
- lib/verbatim/version.rb
|
|
74
|
+
homepage: https://github.com/dsdugal/verbatim
|
|
75
|
+
licenses:
|
|
76
|
+
- MIT
|
|
77
|
+
metadata:
|
|
78
|
+
homepage_uri: https://github.com/dsdugal/verbatim
|
|
79
|
+
source_code_uri: https://github.com/dsdugal/verbatim
|
|
80
|
+
changelog_uri: https://github.com/dsdugal/verbatim/blob/main/CHANGELOG.md
|
|
81
|
+
rubygems_mfa_required: 'true'
|
|
82
|
+
rdoc_options: []
|
|
83
|
+
require_paths:
|
|
84
|
+
- lib
|
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: 3.2.0
|
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '0'
|
|
95
|
+
requirements: []
|
|
96
|
+
rubygems_version: 4.0.9
|
|
97
|
+
specification_version: 4
|
|
98
|
+
summary: Declarative software version schemas
|
|
99
|
+
test_files: []
|