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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8116ea5ced17c3ede88f189de179d0c79cad45852e04df7ad315e58a36c9604
4
- data.tar.gz: 939170153910604499bed507c48bfe3f6d44c0b7909bf716b79471da6e18eb51
3
+ metadata.gz: 4e573bedff931ba6d00a805a59c72c1501b454ce2de48efd1787d4e9d3582541
4
+ data.tar.gz: a33de0609ca8726513b5576f9ce5b9dec7c6ae7a0d9438d3151ce2e03a87c0c3
5
5
  SHA512:
6
- metadata.gz: be410b8d094de0e4070a7a0fe4b6a49117fb60fb6904f17a488878c0374a4e1556280d5ab1ed3d7c917732e0b85c2d077359a94b6842a29bed5ca49c7a1be3e1
7
- data.tar.gz: 93dec0a974b675939584a223eb9e4a551208f942df45bf71bd577f17aa0f6496af27b6a7c381b5cf1cc94958508722cbbddb1570622c9d7fbec6bec1d3842643
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
@@ -1,3 +1,3 @@
1
1
  # flex_args
2
2
 
3
- Flexible Argument Parse
3
+ Flexible Argument Parser
@@ -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
@@ -2,7 +2,7 @@
2
2
 
3
3
  class FlexArgs
4
4
  module Version
5
- VERSION="0.1.1"
5
+ VERSION="0.1.3"
6
6
  end
7
7
  end
8
8
  # SPDX-License-Identifier: AGPL-3.0-or-later
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
- # - Custom Transformations
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
- ESCAPED_COMMA = "a4ey8NGnXIVhsV"
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
- init_values
110
- args.each { parse_arg it }
111
- result
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
- private
140
- def initialize
141
- @alias_definitions = Hash.new
142
- init_values
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 add_flags(shorts)
146
- shorts.each do
147
- flag = alias_definitions.fetch(it, it)
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 add_value(matches)
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
- def cast(value)
161
- case value
162
- when RANGE
163
- Range.new(*value.split("..").map(&:to_i))
164
- when DIGITS
165
- value.to_i
166
- else
167
- value.gsub(ESCAPED_COMMA, ",")
168
- end
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
- def cast_all(value)
172
- value = value.gsub(",,", ESCAPED_COMMA)
173
- values =
174
- value
175
- .split(",")
176
- .map { cast it }
177
- values.one? ? values.first : values
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
- def define_alias(short, long)
181
- alias_definitions.update(short => long)
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
- def init_values
185
- @positionals = []
186
- @flags = Set.new
187
- @values = Hash.new
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 parse_arg(arg)
192
- # require "debug"; binding.break
193
- case arg
194
- when SINGLE_FLAG
195
- flags << Regexp.last_match[1].to_sym
196
- when MANY_FLAGS
197
- add_flags(Regexp.last_match[1].grapheme_clusters)
198
- when VALUE
199
- add_value(Regexp.last_match)
200
- else
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 result
206
- OpenStruct.new(
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.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: