flex_args 0.1.1 → 0.1.3
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 +4 -4
- data/.rdoc_options +27 -0
- data/README.md +1 -1
- data/lib/flex_args/constraint.rb +221 -0
- data/lib/flex_args/enumerable.rb +18 -0
- data/lib/flex_args/parser.rb +125 -0
- data/lib/flex_args/transform.rb +23 -0
- data/lib/flex_args/version.rb +1 -1
- data/lib/flex_args.rb +199 -71
- metadata +6 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4e573bedff931ba6d00a805a59c72c1501b454ce2de48efd1787d4e9d3582541
|
4
|
+
data.tar.gz: a33de0609ca8726513b5576f9ce5b9dec7c6ae7a0d9438d3151ce2e03a87c0c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e6a44342146b8d223a593a2982f2930bec00db7c93ee8f604ddfa10fd0380562d0ce8577bd636e8fe54082acaf49a39627e4c1dd969898a79c363e911bcf968b
|
7
|
+
data.tar.gz: 45955b80f48f89a16d0b64caf3fbb6a14fbbf0e39fb84e625a7201678bd656d246c73c6d6bb86930e4b8e8a3d5bd9ab5fa927ea4489723674cc69f19448bcca5
|
data/.rdoc_options
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
---
|
2
|
+
encoding: UTF-8
|
3
|
+
static_path: []
|
4
|
+
rdoc_include: []
|
5
|
+
page_dir:
|
6
|
+
apply_default_exclude: true
|
7
|
+
autolink_excluded_words: []
|
8
|
+
charset: UTF-8
|
9
|
+
class_module_path_prefix:
|
10
|
+
embed_mixins: false
|
11
|
+
exclude: [tmp/*,coverage/*]
|
12
|
+
file_path_prefix:
|
13
|
+
hyperlink_all: false
|
14
|
+
line_numbers: false
|
15
|
+
locale_dir: locale
|
16
|
+
locale_name:
|
17
|
+
main_page:
|
18
|
+
markup: markdown
|
19
|
+
output_decoration: true
|
20
|
+
show_hash: false
|
21
|
+
skip_tests: true
|
22
|
+
tab_width: 8
|
23
|
+
template_stylesheets: []
|
24
|
+
title:
|
25
|
+
visibility: :protected
|
26
|
+
warn_missing_rdoc_ref: true
|
27
|
+
webcvs:
|
data/README.md
CHANGED
@@ -0,0 +1,221 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class FlexArgs
|
4
|
+
class Constraint
|
5
|
+
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
##
|
9
|
+
# ## Creates a new constraint for the value argument named `name`
|
10
|
+
#
|
11
|
+
# Constraints can be many, which is indicated by the first element
|
12
|
+
# in the `constraints` list, or a constraint can be indicated by
|
13
|
+
# a custom made block.
|
14
|
+
#
|
15
|
+
# The available constraints are:
|
16
|
+
#
|
17
|
+
# ### `Regexp` constraint
|
18
|
+
#
|
19
|
+
# These rdoc_specs show the interface that the `FlexArgs::Parser` uses
|
20
|
+
# to validate constraints, the class is therefore part of the public
|
21
|
+
# API and can be used in isolation where it serves.
|
22
|
+
#
|
23
|
+
# ```spec # regexp
|
24
|
+
# new(:for_some_value, %r{x}).("x") => [:ok, "x"]
|
25
|
+
# new(:for_some_value, %r{x}).("y") => [:error, "regexp constraint for_some_value: (?-mix:x) violated by value \"y\""]
|
26
|
+
# ```
|
27
|
+
#
|
28
|
+
# ### `Range` constraint
|
29
|
+
#
|
30
|
+
# ```spec # range
|
31
|
+
# new(:for_some_int, 1..9).("1") => [:ok, 1]
|
32
|
+
# new(:for_some_int, 1..9).("0") => [:error, "range constraint for_some_int: 1..9 violated by value \"0\""]
|
33
|
+
# new(:for_some_int, 1..9).("y") => [:error, "range constraint for_some_int: 1..9 violated by value \"y\""]
|
34
|
+
#
|
35
|
+
# new(:for_string, "a".."c").("a") => [:ok, "a"]
|
36
|
+
# new(:for_string, "a".."c").("d") => [:error, 'member constraint for_string: "a".."c" violated by value "d"']
|
37
|
+
#
|
38
|
+
# ```
|
39
|
+
#
|
40
|
+
# **N.B.** that ranges not of type integer are treated as `Member` contraints below.
|
41
|
+
#
|
42
|
+
# ### `Member` constraint
|
43
|
+
#
|
44
|
+
# While the `Range` constraint above does int type casting if needed, the `Member` constraint cannot do
|
45
|
+
# that, because it cannot access the type of the container against which membership will be tested.
|
46
|
+
#
|
47
|
+
# `Member` constraints can be triggered by providing a `Set`, `Array` or `Hash` second parameter
|
48
|
+
#
|
49
|
+
# ```spec # Various membership rdoc specs
|
50
|
+
# vowels = %w[a e i o u y]
|
51
|
+
# hashy = Hash[vowels.product([true])]
|
52
|
+
# set = Set.new(vowels)
|
53
|
+
# in_array = new(:in_array, vowels)
|
54
|
+
# in_set = new(:in_set, set)
|
55
|
+
# in_hash = new(:in_hash, hashy)
|
56
|
+
#
|
57
|
+
# expect(in_array.("a")).to eq([:ok, "a"])
|
58
|
+
# expect(in_array.("y")).to eq([:ok, "y"])
|
59
|
+
# expect(in_set.("y")).to eq([:ok, "y"])
|
60
|
+
# expect(in_hash.("o")).to eq([:ok, "o"])
|
61
|
+
#
|
62
|
+
# expect(in_array.("b"))
|
63
|
+
# .to eq([:error, "member constraint in_array: #{vowels.inspect} violated by value \"b\""])
|
64
|
+
#
|
65
|
+
# expect(in_hash.("ec"))
|
66
|
+
# .to eq([:error, "member constraint in_hash: #{hashy.inspect} violated by value \"ec\""])
|
67
|
+
#
|
68
|
+
# expect(in_set.("aa"))
|
69
|
+
# .to eq([:error, "member constraint in_set: #{set.inspect} violated by value \"aa\""])
|
70
|
+
# ```
|
71
|
+
#
|
72
|
+
# ### Custom constraint
|
73
|
+
#
|
74
|
+
# This is easily implemented by passing a block which will return a pair as described above, but also note
|
75
|
+
# two more features of the custom constraint
|
76
|
+
#
|
77
|
+
# - exceptions will be caught and transformed into an error, thusly simplyfing the code block:
|
78
|
+
# - and values can be transformed
|
79
|
+
#
|
80
|
+
# ```spec # custom
|
81
|
+
# constraint = new :even do
|
82
|
+
# value = Integer(it)
|
83
|
+
# value.even? ? [:ok, value / 2] : [:error, "not even"]
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# expect(constraint.("84")).to eq([:ok, 42])
|
87
|
+
# expect(constraint.("21")).to eq([:error, "custom constraint even: violated by value \"21\" with message: \"not even\""])
|
88
|
+
#
|
89
|
+
# exception_message = '"invalid value for Integer(): \\"abc\\" (ArgumentError)"'
|
90
|
+
# complete_message = "custom constraint even: violated by value \"abc\" with message: #{exception_message}"
|
91
|
+
# expect(constraint.("abc")).to eq([:error, complete_message])
|
92
|
+
# ```
|
93
|
+
#
|
94
|
+
# Also note that the correct format of the block's return value is important
|
95
|
+
#
|
96
|
+
# ```spec # enforce correct format of custom constraint's block
|
97
|
+
# constraint = new(:bad) { 42 }
|
98
|
+
#
|
99
|
+
# expect { constraint.(nil) }
|
100
|
+
# .to raise_error(ArgumentError, "badly formatted custom block for custom constraint bad returned 42, needed [:ok|:error, value_or_message]")
|
101
|
+
# ```
|
102
|
+
#
|
103
|
+
def initialize(name, *constraints, &block)
|
104
|
+
@name = name
|
105
|
+
case constraints
|
106
|
+
in [Regexp => constraint]
|
107
|
+
_init_regexp(name, constraint)
|
108
|
+
in [Range => range]
|
109
|
+
_init_range(name, range)
|
110
|
+
in [Array | Hash | Set => container]
|
111
|
+
_init_membership(name, container, false)
|
112
|
+
in [Array | Hash | Set => container, :autocast]
|
113
|
+
_init_membership(name, container, true)
|
114
|
+
in [:member, container]
|
115
|
+
_init_membership(name, container)
|
116
|
+
else
|
117
|
+
raise ArgumentError,
|
118
|
+
"illegal constraint spec #{constraints.inspect} for value #{name}" unless block
|
119
|
+
_init_custom(name, &block)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def call(value)
|
124
|
+
@constrainer.(value)
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def _init_custom(name, &block)
|
130
|
+
bad_format = false
|
131
|
+
@constrainer = -> value do
|
132
|
+
case block.(value)
|
133
|
+
in [:error, message]
|
134
|
+
_make_error(:custom, name, nil, value, message)
|
135
|
+
in [:ok, _] => result
|
136
|
+
result
|
137
|
+
in result
|
138
|
+
bad_format = true
|
139
|
+
raise ArgumentError, "badly formatted custom block for custom constraint #{name} returned #{result.inspect}, needed [:ok|:error, value_or_message]"
|
140
|
+
end
|
141
|
+
rescue Exception => e
|
142
|
+
raise if bad_format
|
143
|
+
_make_error(:custom, name, nil, value, "#{e.message} (#{e.class})")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def _init_membership(name, container, autocast)
|
148
|
+
@constrainer = -> value do
|
149
|
+
if container.member?(value)
|
150
|
+
[:ok, value]
|
151
|
+
elsif autocast
|
152
|
+
case _to_int(value)
|
153
|
+
in [:ok, int_val]
|
154
|
+
if container.member(int_val)
|
155
|
+
[:ok, int_val]
|
156
|
+
else
|
157
|
+
_make_error(:member, name, container.inspect, int_value)
|
158
|
+
end
|
159
|
+
else
|
160
|
+
_make_error(:member, name, container.inspect, value)
|
161
|
+
end
|
162
|
+
else
|
163
|
+
_make_error(:member, name, container.inspect, value)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def _init_range(name, range)
|
169
|
+
if Integer === range.first
|
170
|
+
_init_int_range(name, range)
|
171
|
+
else
|
172
|
+
_init_membership(name, range, false)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def _init_int_range(name, range)
|
177
|
+
@constrainer = -> value do
|
178
|
+
case _to_int(value)
|
179
|
+
in [:ok, int_val]
|
180
|
+
if range === int_val
|
181
|
+
[:ok, int_val]
|
182
|
+
else
|
183
|
+
_make_error(:range, name, range, value)
|
184
|
+
end
|
185
|
+
else
|
186
|
+
_make_error(:range, name, range, value)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def _init_regexp(name, constraint)
|
192
|
+
@constrainer = -> value do
|
193
|
+
if constraint.match?(value)
|
194
|
+
[:ok, value]
|
195
|
+
else
|
196
|
+
[:error, "regexp constraint #{name}: #{constraint} violated by value #{value.inspect}"]
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def _make_error(type, name, constraint_desc, value, message = nil)
|
202
|
+
message = [
|
203
|
+
type,
|
204
|
+
"constraint",
|
205
|
+
"#{name}:",
|
206
|
+
constraint_desc,
|
207
|
+
"violated by value",
|
208
|
+
value.inspect,
|
209
|
+
(message ? "with message: #{message.inspect}" : nil)
|
210
|
+
].compact.join(" ")
|
211
|
+
[:error, message]
|
212
|
+
end
|
213
|
+
|
214
|
+
def _to_int(value)
|
215
|
+
[:ok, Integer(value)]
|
216
|
+
rescue ArgumentError
|
217
|
+
:error
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Enumerable
|
4
|
+
def reduce_while(init, tag=nil, &block)
|
5
|
+
result =
|
6
|
+
reduce(init) do |i, e|
|
7
|
+
case block.(i, e)
|
8
|
+
in [:halt, value]
|
9
|
+
return value
|
10
|
+
in [:cont, value]
|
11
|
+
value
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
tag ? [tag, result] : result
|
16
|
+
end
|
17
|
+
end
|
18
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'enumerable'
|
4
|
+
|
5
|
+
class FlexArgs
|
6
|
+
class Parser
|
7
|
+
attr_reader :args, :errors, :flags, :flex_args, :positionals, :values_from_args
|
8
|
+
|
9
|
+
ESCAPED_COMMA = "a4ey8NGnXIVhsV"
|
10
|
+
DIGITS = %r{\A [-+]? \d+ \z}x
|
11
|
+
MANY_FLAGS = %r{\A - (.*) \z}x
|
12
|
+
RANGE = %r{\A [-+]? \d+ \.\. [-+]? \d+ \z}x
|
13
|
+
SINGLE_FLAG = %r{\A -- (.*) \z}x
|
14
|
+
VALUE = %r{\A ([[:alpha:]][[:alnum:]_]*) : (.*) \z}x
|
15
|
+
|
16
|
+
def parse(args)
|
17
|
+
init_values
|
18
|
+
args.each { parse_arg it }
|
19
|
+
return [:ok, result] if errors.empty?
|
20
|
+
|
21
|
+
[:error, errors]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def initialize(flex_args)
|
26
|
+
@flex_args = flex_args
|
27
|
+
init_values
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_flags(shorts)
|
31
|
+
shorts.each do
|
32
|
+
flag = flex_args.alias_definitions.fetch(it, it)
|
33
|
+
flags << flag.to_sym
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_value(matches)
|
38
|
+
matches => [key, value]
|
39
|
+
key = key.to_sym
|
40
|
+
check_allowed!(key)
|
41
|
+
case check_constraints!(key, value)
|
42
|
+
in [:error, message]
|
43
|
+
errors << message
|
44
|
+
in [:ok, value]
|
45
|
+
values_from_args
|
46
|
+
.update(key => cast_all(value)) do |k, o, n|
|
47
|
+
Array(o) << n
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def cast(value)
|
53
|
+
case value
|
54
|
+
when RANGE
|
55
|
+
Range.new(*value.split("..").map(&:to_i))
|
56
|
+
when DIGITS
|
57
|
+
value.to_i
|
58
|
+
else
|
59
|
+
value.gsub(ESCAPED_COMMA, ",")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def cast_all(value)
|
64
|
+
return value unless String === value
|
65
|
+
value = value.gsub(",,", ESCAPED_COMMA)
|
66
|
+
values =
|
67
|
+
value
|
68
|
+
.split(",")
|
69
|
+
.map { cast it }
|
70
|
+
values.one? ? values.first : values
|
71
|
+
end
|
72
|
+
|
73
|
+
def check_allowed!(key)
|
74
|
+
errors << "unallowed value arg #{key}:" unless flex_args.allowed?(key)
|
75
|
+
end
|
76
|
+
|
77
|
+
def check_constraints!(key, value)
|
78
|
+
constraints = flex_args.constraints(key)
|
79
|
+
constraints.reduce_while(value, :ok) do |v, constraint|
|
80
|
+
case constraint.(v)
|
81
|
+
in [:ok, val]
|
82
|
+
[:cont, val]
|
83
|
+
in [:error, message]
|
84
|
+
[:halt, [:error, message]]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def init_values
|
90
|
+
@errors = []
|
91
|
+
@flags = Set.new
|
92
|
+
@positionals = []
|
93
|
+
@values_from_args = Hash.new
|
94
|
+
end
|
95
|
+
|
96
|
+
def parse_arg(arg)
|
97
|
+
# require "debug"; binding.break
|
98
|
+
case arg
|
99
|
+
when SINGLE_FLAG
|
100
|
+
flags << Regexp.last_match[1].to_sym
|
101
|
+
when MANY_FLAGS
|
102
|
+
add_flags(Regexp.last_match[1].grapheme_clusters)
|
103
|
+
when VALUE
|
104
|
+
add_value(Regexp.last_match)
|
105
|
+
else
|
106
|
+
positionals << arg
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def result
|
111
|
+
OpenStruct.new(
|
112
|
+
positionals: positionals,
|
113
|
+
flags: flags,
|
114
|
+
values: values,
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
def values
|
119
|
+
flex_args
|
120
|
+
.default_values
|
121
|
+
.merge(values_from_args)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class FlexArgs
|
4
|
+
class Transform
|
5
|
+
|
6
|
+
def call(value) = [:ok, @transformer.(value)]
|
7
|
+
|
8
|
+
private
|
9
|
+
def initialize(*transformers, &transformer)
|
10
|
+
case transformers
|
11
|
+
in [_]
|
12
|
+
raise ArgumentError, "NOT YET IMPLEMENTED"
|
13
|
+
else
|
14
|
+
_init_transformer(&transformer)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def _init_transformer(&transformer)
|
19
|
+
@transformer = transformer
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
data/lib/flex_args/version.rb
CHANGED
data/lib/flex_args.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'ostruct'
|
4
|
+
require_relative 'flex_args/constraint'
|
5
|
+
require_relative 'flex_args/parser'
|
6
|
+
require_relative 'flex_args/transform'
|
4
7
|
|
5
8
|
##
|
6
9
|
# ## Abstract
|
@@ -14,11 +17,11 @@ require 'ostruct'
|
|
14
17
|
# ### Positional arguments
|
15
18
|
#
|
16
19
|
# Are all arguments that do not:
|
17
|
-
#
|
20
|
+
#
|
18
21
|
# - start with a `-`
|
19
22
|
# - contain any of the following characters: `:,`
|
20
23
|
# - contain the string `".."`
|
21
|
-
#
|
24
|
+
#
|
22
25
|
# Therefore the following arguments will be parsed as
|
23
26
|
#
|
24
27
|
# ```spec # Only default positional arguments
|
@@ -89,28 +92,26 @@ require 'ostruct'
|
|
89
92
|
# - required
|
90
93
|
# - domain checks
|
91
94
|
# - format checks
|
92
|
-
# -
|
93
|
-
# - Semantic Checks (v0.2)
|
95
|
+
# - Transformations
|
96
|
+
# - Semantic Checks - between multiple values (v0.2)
|
94
97
|
#
|
95
98
|
# and they are documented in the rdocs of the corresponding methods
|
96
99
|
#
|
97
100
|
class FlexArgs
|
98
101
|
|
99
|
-
|
100
|
-
DIGITS = %r{\A [-+]? \d+ \z}x
|
101
|
-
MANY_FLAGS = %r{\A - (.*) \z}x
|
102
|
-
RANGE = %r{\A [-+]? \d+ \.\. [-+]? \d+ \z}x
|
103
|
-
SINGLE_FLAG = %r{\A -- (.*) \z}x
|
104
|
-
VALUE = %r{\A ([[:alpha:]][[:alnum:]_]+) : (.*) \z}x
|
105
|
-
|
106
|
-
attr_reader :alias_definitions, :flags, :positionals, :result, :values
|
102
|
+
attr_reader :alias_definitions, :default_values, :result
|
107
103
|
|
108
104
|
def parse(args)
|
109
|
-
|
110
|
-
|
111
|
-
|
105
|
+
result = Parser.new(self).parse(args)
|
106
|
+
case result
|
107
|
+
in [:ok, result1]
|
108
|
+
result1
|
109
|
+
in [:error, errors]
|
110
|
+
raise ArgumentError, errors.join("\n")
|
111
|
+
end
|
112
112
|
end
|
113
113
|
|
114
|
+
|
114
115
|
##
|
115
116
|
#
|
116
117
|
# ## Aliases are defined by passing a hash
|
@@ -136,78 +137,205 @@ class FlexArgs
|
|
136
137
|
self
|
137
138
|
end
|
138
139
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
140
|
+
##
|
141
|
+
# ## Allowed values and flags
|
142
|
+
#
|
143
|
+
# As soon as this method is called the parser will
|
144
|
+
# check that **all** values in args are allowed
|
145
|
+
#
|
146
|
+
# Values are either identified by their name (a `Symbol`)
|
147
|
+
# or by their name and default value (a pair of `Symbol` and
|
148
|
+
# any value
|
149
|
+
#
|
150
|
+
# ```spec
|
151
|
+
# parser = FlexArgs
|
152
|
+
# .new
|
153
|
+
# .allow(:min, [:max, 3])
|
154
|
+
#
|
155
|
+
# expect(parser.parse(%w[min:1]).values)
|
156
|
+
# .to eq(min: 1, max: 3)
|
157
|
+
# ```
|
158
|
+
#
|
159
|
+
# Therefore the following args are illegal and an `ArgumentError`
|
160
|
+
# is raised
|
161
|
+
#
|
162
|
+
# ```spec # Argument Error for unallowed value
|
163
|
+
# expect do
|
164
|
+
# FlexArgs
|
165
|
+
# .new
|
166
|
+
# .allow(:n)
|
167
|
+
# .parse(%w[a:2])
|
168
|
+
# end
|
169
|
+
# .to raise_error(
|
170
|
+
# ArgumentError,
|
171
|
+
# "unallowed value arg a:"
|
172
|
+
# )
|
173
|
+
#
|
174
|
+
# ```
|
175
|
+
#
|
176
|
+
def allow(*values)
|
177
|
+
values.each { allow_value it }
|
178
|
+
self
|
143
179
|
end
|
144
180
|
|
145
|
-
def
|
146
|
-
|
147
|
-
|
148
|
-
flags << flag.to_sym
|
149
|
-
end
|
181
|
+
def allowed?(value)
|
182
|
+
return true unless @allowed_values
|
183
|
+
@allowed_values.member?(value)
|
150
184
|
end
|
151
185
|
|
152
|
-
def
|
153
|
-
matches => [key, value]
|
154
|
-
values
|
155
|
-
.update(key.to_sym => cast_all(value)) do |k, o, n|
|
156
|
-
Array(o) << n
|
157
|
-
end
|
158
|
-
end
|
186
|
+
def constraints(key) = @constraints[key]
|
159
187
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
188
|
+
##
|
189
|
+
# ## Constraints
|
190
|
+
#
|
191
|
+
# Constraints are defined with the `constraint` method.
|
192
|
+
#
|
193
|
+
# **N.B.** defined Constraints **after** allow unless you
|
194
|
+
# want to explicitly add the value arg with `allow`
|
195
|
+
#
|
196
|
+
# ### Simple forms
|
197
|
+
#
|
198
|
+
# #### Regexp constraint
|
199
|
+
#
|
200
|
+
# ```spec # regexp constraints
|
201
|
+
# parser = FlexArgs
|
202
|
+
# .new
|
203
|
+
# .constrain(:n, %r{\A \d+ \z}x)
|
204
|
+
#
|
205
|
+
# expect(parser.parse(%w[n:42]).values).to eq(n: 42)
|
206
|
+
#
|
207
|
+
# expect { parser.parse(%w[n:42a]) }
|
208
|
+
# .to raise_error(ArgumentError, 'regexp constraint n: (?x-mi:\A \d+ \z) violated by value "42a"')
|
209
|
+
#
|
210
|
+
# ```
|
211
|
+
#
|
212
|
+
# #### Range constraints
|
213
|
+
#
|
214
|
+
# ```spec # range constraints
|
215
|
+
# parser = FlexArgs
|
216
|
+
# .new
|
217
|
+
# .constrain(:n, 2..3)
|
218
|
+
#
|
219
|
+
# expect(parser.parse(%w[n:2]).values).to eq(n: 2)
|
220
|
+
#
|
221
|
+
# expect { parser.parse(%w[n:42a]) }
|
222
|
+
# .to raise_error(ArgumentError, 'range constraint n: 2..3 violated by value "42a"')
|
223
|
+
#
|
224
|
+
# expect { parser.parse(%w[n:42]) }
|
225
|
+
# .to raise_error(ArgumentError, 'range constraint n: 2..3 violated by value "42"')
|
226
|
+
#
|
227
|
+
# ```
|
228
|
+
#
|
229
|
+
# #### Combining Constraints
|
230
|
+
#
|
231
|
+
# More than one constraint can be imposed on a value
|
232
|
+
#
|
233
|
+
# Firstly one can combine any constraint with a custom constraint.
|
234
|
+
#
|
235
|
+
# **N.B.** That in this case the custom constraint is executed last, therefore
|
236
|
+
#
|
237
|
+
# ```spec # Order of constraints
|
238
|
+
# parser = FlexArgs.new
|
239
|
+
# .constrain(:n, 1..10) { it == 5 ? [:ok, 100] : [:ok, 2*it] }
|
240
|
+
#
|
241
|
+
# expect(parser.parse(%w[n:5]).values).to eq(n: 100)
|
242
|
+
#
|
243
|
+
# ```
|
244
|
+
#
|
245
|
+
# However if we had defined the custom constraint (which actually is a transformer
|
246
|
+
# as it _always_ returns an `:ok` value, the range constraint would fail
|
247
|
+
#
|
248
|
+
# ```spec # Bad order
|
249
|
+
# parser = FlexArgs.new
|
250
|
+
# .constrain(:n) { it == 5 ? [:ok, 100] : [:ok, 2*it] }
|
251
|
+
# .constrain(:n, 1..10)
|
252
|
+
#
|
253
|
+
# expect { parser.parse(%w[n:5]) }
|
254
|
+
# .to raise_error(ArgumentError)
|
255
|
+
#
|
256
|
+
# ```
|
257
|
+
#
|
258
|
+
# As mentioned before, custom constraints that always return `:ok` values are indeed
|
259
|
+
# _transformers_ and can be written more with the `transform` [method below](/FlexArgs.html#method-i-transform-label-Transformers)
|
260
|
+
#
|
261
|
+
#
|
262
|
+
# Please find the documentation about more constraints [here](/FlexArgs/Constraint.html)
|
263
|
+
#
|
264
|
+
def constrain(value, *constraints, &block)
|
265
|
+
@allowed_values << value.to_sym if @allowed_values
|
266
|
+
@constraints[value.to_sym] << Constraint.new(value, *constraints) unless constraints.empty?
|
267
|
+
@constraints[value.to_sym] << Constraint.new(value, &block) if block
|
268
|
+
self
|
169
269
|
end
|
170
270
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
271
|
+
##
|
272
|
+
# ## Transformers
|
273
|
+
#
|
274
|
+
# Transformers and Constraints are executed in the order they are defined
|
275
|
+
#
|
276
|
+
# ```spec # Transformer assures constraint passes
|
277
|
+
#
|
278
|
+
# parser = FlexArgs.new
|
279
|
+
# .transform(:word) { it.downcase }
|
280
|
+
# .constrain(:word, "a".."z")
|
281
|
+
#
|
282
|
+
# expect(parser.parse(%w[word:A]).values).to eq(word: "a")
|
283
|
+
# ```
|
284
|
+
#
|
285
|
+
# And therefore
|
286
|
+
#
|
287
|
+
# ```spec # Transformer called too late to assure constraint passes
|
288
|
+
#
|
289
|
+
# parser = FlexArgs.new
|
290
|
+
# .constrain(:word, "a".."z")
|
291
|
+
# .transform(:word) { it.downcase }
|
292
|
+
#
|
293
|
+
# expect { parser.parse(%w[word:A]) }.to raise_error(ArgumentError)
|
294
|
+
# ```
|
295
|
+
def transform(value, *transforms, &transformer)
|
296
|
+
@allowed_values << value.to_sym if @allowed_values
|
297
|
+
@constraints[value.to_sym] << Transform.new(*transforms) unless transforms.empty?
|
298
|
+
@constraints[value.to_sym] << Transform.new(&transformer) if transformer
|
299
|
+
self
|
178
300
|
end
|
179
301
|
|
180
|
-
|
181
|
-
|
302
|
+
##
|
303
|
+
#
|
304
|
+
# Just returns the parsed values, or their default values if
|
305
|
+
# a default was provided and the value was not present in
|
306
|
+
# the provided arguments.
|
307
|
+
def values
|
308
|
+
default_values.merge(values_from_args)
|
182
309
|
end
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
@
|
187
|
-
@
|
310
|
+
|
311
|
+
private
|
312
|
+
def initialize
|
313
|
+
@alias_definitions = Hash.new
|
314
|
+
@allowed_values = nil
|
315
|
+
@constraints = Hash.new { |h, k| h[k] = [] }
|
316
|
+
@default_values = Hash.new
|
188
317
|
end
|
189
318
|
|
319
|
+
def add_default_value(name, value)
|
320
|
+
default_values.update(name => value)
|
321
|
+
end
|
190
322
|
|
191
|
-
def
|
192
|
-
|
193
|
-
case
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
positionals << arg
|
323
|
+
def allow_value(value_def)
|
324
|
+
@allowed_values ||= Set.new
|
325
|
+
case value_def
|
326
|
+
in Symbol
|
327
|
+
@allowed_values << value_def
|
328
|
+
in [Symbol => value_name, default_value]
|
329
|
+
@allowed_values << value_name
|
330
|
+
add_default_value(value_name, default_value)
|
331
|
+
in _
|
332
|
+
raise ArgumentError, "arg must be of format Symbol or [Symbol, value], but was #{value_def}"
|
202
333
|
end
|
203
334
|
end
|
204
335
|
|
205
|
-
def
|
206
|
-
|
207
|
-
positionals: positionals,
|
208
|
-
flags: flags,
|
209
|
-
values: values,
|
210
|
-
)
|
336
|
+
def define_alias(short, long)
|
337
|
+
alias_definitions.update(short => long)
|
211
338
|
end
|
339
|
+
|
212
340
|
end
|
213
341
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flex_args
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robert Dober
|
@@ -31,9 +31,14 @@ executables: []
|
|
31
31
|
extensions: []
|
32
32
|
extra_rdoc_files: []
|
33
33
|
files:
|
34
|
+
- ".rdoc_options"
|
34
35
|
- LICENSE
|
35
36
|
- README.md
|
36
37
|
- lib/flex_args.rb
|
38
|
+
- lib/flex_args/constraint.rb
|
39
|
+
- lib/flex_args/enumerable.rb
|
40
|
+
- lib/flex_args/parser.rb
|
41
|
+
- lib/flex_args/transform.rb
|
37
42
|
- lib/flex_args/version.rb
|
38
43
|
homepage: https://codeberg.org/lab419/flex_args
|
39
44
|
licenses:
|