flex_args 0.1.0 → 0.1.2

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: ef1ef86d73320f89eba86a8e982f3b7d24724ccdfe0d085ac2a9ac674485b79e
4
- data.tar.gz: 0f554da437841db0d31aee39352d15db3e6480b95bcbcf8755ac5dcf77d84cbb
3
+ metadata.gz: 5700aa1d881f51e7f2f44d30075e68367202e010ec71779193042f7efea70d95
4
+ data.tar.gz: aa46d04734fed4cf7776c3fe0c18c8fad391f5dc66a446c4fc664b8284deec79
5
5
  SHA512:
6
- metadata.gz: f745f4e480196273b41b4db03742f3ba1a8d1a1d4b645d81b2aa976a30b7709c7971871ab6edea421d0575878c24de931055c60a39b20f54adbb1dc8bba201b9
7
- data.tar.gz: 74e53271bf7ca6699ea7dbf47b2bf31602e3ae2a8b8fbb1595d58d0e94ddb1970f1ff1087b83946f0169af025c0d4e346cb81f66a8b5f4c878c9f226587c8b2c
6
+ metadata.gz: bc38f0a24e8d3589adacadabfeac2e1dfa7ce84ec2bd7b9def3fa6d2ef5bc2159ac1856136da273ea8a0f34c072ecfbb7008925544f9c27dec93edbf37206791
7
+ data.tar.gz: c349ad91b2f10bb6b463d88388cd34c7f48fba0c736de18754545b88f6aa0f02f5ec60e39ea685537046cf6629bae9188b12531935a98f2bd7db37b8902223da
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/*]
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:
@@ -0,0 +1,150 @@
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
+ # ```
36
+ # ### Custom constraint
37
+ #
38
+ # This is easily implemented by passing a block which will return a pair as described above, but also note
39
+ # two more features of the custom constraint
40
+ #
41
+ # - exceptions will be caught and transformed into an error, thusly simplyfing the code block:
42
+ # - and values can be transformed
43
+ #
44
+ # ```spec # custom
45
+ # constraint = new :even do
46
+ # value = Integer(it)
47
+ # value.even? ? [:ok, value / 2] : [:error, "not even"]
48
+ # end
49
+ #
50
+ # expect(constraint.("84")).to eq([:ok, 42])
51
+ # expect(constraint.("21")).to eq([:error, "custom constraint even: violated by value \"21\" with message: \"not even\""])
52
+ #
53
+ # exception_message = '"invalid value for Integer(): \\"abc\\" (ArgumentError)"'
54
+ # complete_message = "custom constraint even: violated by value \"abc\" with message: #{exception_message}"
55
+ # expect(constraint.("abc")).to eq([:error, complete_message])
56
+ # ```
57
+ #
58
+ # Also note that the correct format of the block's return value is important
59
+ #
60
+ # ```spec # enforce correct format of custom constraint's block
61
+ # constraint = new(:bad) { 42 }
62
+ #
63
+ # expect { constraint.(nil) }
64
+ # .to raise_error(ArgumentError, "badly formatted custom block for custom constraint bad returned 42, needed [:ok|:error, value_or_message]")
65
+ # ```
66
+ #
67
+ def initialize(name, *constraints, &block)
68
+ @name = name
69
+ case constraints
70
+ in [Regexp => constraint]
71
+ _init_regexp(name, constraint)
72
+ in [Range => range]
73
+ _init_range(name, range)
74
+ else
75
+ raise ArgumentError,
76
+ "illegal constraint spec #{constraints.inspect} for value #{name}" unless block
77
+ _init_custom(name, &block)
78
+ end
79
+ end
80
+
81
+ def call(value)
82
+ @constrainer.(value)
83
+ end
84
+
85
+ private
86
+
87
+ def _init_custom(name, &block)
88
+ bad_format = false
89
+ @constrainer = -> value do
90
+ case block.(value)
91
+ in [:error, message]
92
+ _make_error(:custom, name, nil, value, message)
93
+ in [:ok, _] => result
94
+ result
95
+ in result
96
+ bad_format = true
97
+ raise ArgumentError, "badly formatted custom block for custom constraint #{name} returned #{result.inspect}, needed [:ok|:error, value_or_message]"
98
+ end
99
+ rescue Exception => e
100
+ raise if bad_format
101
+ _make_error(:custom, name, nil, value, "#{e.message} (#{e.class})")
102
+ end
103
+ end
104
+
105
+ def _init_range(name, range)
106
+ @constrainer = -> value do
107
+ case _to_int(value)
108
+ in [:ok, int_val]
109
+ if range === int_val
110
+ [:ok, int_val]
111
+ else
112
+ _make_error(:range, name, range, value)
113
+ end
114
+ else
115
+ _make_error(:range, name, range, value)
116
+ end
117
+ end
118
+ end
119
+
120
+ def _init_regexp(name, constraint)
121
+ @constrainer = -> value do
122
+ if constraint.match?(value)
123
+ [:ok, value]
124
+ else
125
+ [:error, "regexp constraint #{name}: #{constraint} violated by value #{value.inspect}"]
126
+ end
127
+ end
128
+ end
129
+
130
+ def _make_error(type, name, constraint_desc, value, message = nil)
131
+ message = [
132
+ type,
133
+ "constraint",
134
+ "#{name}:",
135
+ constraint_desc,
136
+ "violated by value",
137
+ value.inspect,
138
+ (message ? "with message: #{message.inspect}" : nil)
139
+ ].compact.join(" ")
140
+ [:error, message]
141
+ end
142
+
143
+ def _to_int(value)
144
+ [:ok, Integer(value)]
145
+ rescue ArgumentError
146
+ :error
147
+ end
148
+ end
149
+ end
150
+ # 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
@@ -2,7 +2,7 @@
2
2
 
3
3
  class FlexArgs
4
4
  module Version
5
- VERSION="0.1.0"
5
+ VERSION="0.1.2"
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,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'ostruct'
4
+ require_relative 'flex_args/constraint'
5
+ require_relative 'flex_args/parser'
4
6
 
5
7
  ##
6
8
  # ## Abstract
@@ -14,11 +16,11 @@ require 'ostruct'
14
16
  # ### Positional arguments
15
17
  #
16
18
  # Are all arguments that do not:
17
- #
19
+ #
18
20
  # - start with a `-`
19
21
  # - contain any of the following characters: `:,`
20
22
  # - contain the string `".."`
21
- #
23
+ #
22
24
  # Therefore the following arguments will be parsed as
23
25
  #
24
26
  # ```spec # Only default positional arguments
@@ -31,7 +33,7 @@ require 'ostruct'
31
33
  # ```spec # Flags and positionals
32
34
  # args = parse(%w[hello --verbose -world 42])
33
35
  # expect(args.positionals).to eq(%w[hello 42])
34
- # expect(args.flags).to eq(Set.new(["verbose", "w", "o", "r", "l", "d"]))
36
+ # expect(args.flags).to eq(Set.new(%i[verbose w o r l d]))
35
37
  # ```
36
38
  # ### Value arguments
37
39
  #
@@ -52,7 +54,7 @@ require 'ostruct'
52
54
  # parsed = parse(input)
53
55
  # expect(parsed.positionals).to eq(["a/b.rb"])
54
56
  # expect(parsed.values).to eq(range: -2..3, offset: 3)
55
- # expect(parsed.flags).to eq(Set.new(["V"]))
57
+ # expect(parsed.flags).to eq(Set.new([:V]))
56
58
  # ```
57
59
  #
58
60
  # #### Lists
@@ -96,21 +98,19 @@ require 'ostruct'
96
98
  #
97
99
  class FlexArgs
98
100
 
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
101
+ attr_reader :alias_definitions, :default_values, :result
107
102
 
108
103
  def parse(args)
109
- init_values
110
- args.each { parse_arg it }
111
- result
104
+ result = Parser.new(self).parse(args)
105
+ case result
106
+ in [:ok, result1]
107
+ result1
108
+ in [:error, errors]
109
+ raise ArgumentError, errors.join("\n")
110
+ end
112
111
  end
113
112
 
113
+
114
114
  ##
115
115
  #
116
116
  # ## Aliases are defined by passing a hash
@@ -136,78 +136,140 @@ class FlexArgs
136
136
  self
137
137
  end
138
138
 
139
- private
140
- def initialize
141
- @alias_definitions = Hash.new
142
- init_values
139
+ ##
140
+ # ## Allowed values and flags
141
+ #
142
+ # As soon as this method is called the parser will
143
+ # check that **all** values in args are allowed
144
+ #
145
+ # Values are either identified by their name (a `Symbol`)
146
+ # or by their name and default value (a pair of `Symbol` and
147
+ # any value
148
+ #
149
+ # ```spec
150
+ # parser = FlexArgs
151
+ # .new
152
+ # .allow(:min, [:max, 3])
153
+ #
154
+ # expect(parser.parse(%w[min:1]).values)
155
+ # .to eq(min: 1, max: 3)
156
+ # ```
157
+ #
158
+ # Therefore the following args are illegal and an `ArgumentError`
159
+ # is raised
160
+ #
161
+ # ```spec # Argument Error for unallowed value
162
+ # expect do
163
+ # FlexArgs
164
+ # .new
165
+ # .allow(:n)
166
+ # .parse(%w[a:2])
167
+ # end
168
+ # .to raise_error(
169
+ # ArgumentError,
170
+ # "unallowed value arg a:"
171
+ # )
172
+ #
173
+ # ```
174
+ #
175
+ def allow(*values)
176
+ values.each { allow_value it }
177
+ self
143
178
  end
144
179
 
145
- def add_flags(shorts)
146
- shorts.each do
147
- flag = alias_definitions.fetch(it, it)
148
- flags << flag
149
- end
180
+ def allowed?(value)
181
+ return true unless @allowed_values
182
+ @allowed_values.member?(value)
150
183
  end
151
184
 
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
185
+ def constraints(key) = @constraints[key]
159
186
 
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
187
+ ##
188
+ # ## Constraints
189
+ #
190
+ # Constraints are defined with the `constraint` method.
191
+ #
192
+ # **N.B.** defined Constraints **after** allow unless you
193
+ # want to explicitly add the value arg with `allow`
194
+ #
195
+ # ### Simple forms
196
+ #
197
+ # #### Regexp constraint
198
+ #
199
+ # ```spec # regexp constraints
200
+ # parser = FlexArgs
201
+ # .new
202
+ # .constrain(:n, %r{\A \d+ \z}x)
203
+ #
204
+ # expect(parser.parse(%w[n:42]).values).to eq(n: 42)
205
+ #
206
+ # expect { parser.parse(%w[n:42a]) }
207
+ # .to raise_error(ArgumentError, 'regexp constraint n: (?x-mi:\A \d+ \z) violated by value "42a"')
208
+ #
209
+ # ```
210
+ #
211
+ # #### Range constraints
212
+ #
213
+ # ```spec # range constraints
214
+ # parser = FlexArgs
215
+ # .new
216
+ # .constrain(:n, 2..3)
217
+ #
218
+ # expect(parser.parse(%w[n:2]).values).to eq(n: 2)
219
+ #
220
+ # expect { parser.parse(%w[n:42a]) }
221
+ # .to raise_error(ArgumentError, 'range constraint n: 2..3 violated by value "42a"')
222
+ #
223
+ # expect { parser.parse(%w[n:42]) }
224
+ # .to raise_error(ArgumentError, 'range constraint n: 2..3 violated by value "42"')
225
+ #
226
+ # ```
227
+ #
228
+ # Please find the documentation about more constraints [here](Constraint.html)
229
+ #
230
+ def constrain(value, *constraints, &block)
231
+ @allowed_values << value.to_sym if @allowed_values
232
+ @constraints[value.to_sym] << Constraint.new(value, *constraints, &block)
233
+ self
169
234
  end
170
235
 
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
236
+ ##
237
+ #
238
+ # Just returns the parsed values, or their default values if
239
+ # a default was provided and the value was not present in
240
+ # the provided arguments.
241
+ def values
242
+ default_values.merge(values_from_args)
178
243
  end
179
244
 
180
- def define_alias(short, long)
181
- alias_definitions.update(short => long)
245
+ private
246
+ def initialize
247
+ @alias_definitions = Hash.new
248
+ @allowed_values = nil
249
+ @constraints = Hash.new { |h, k| h[k] = [] }
250
+ @default_values = Hash.new
182
251
  end
183
252
 
184
- def init_values
185
- @positionals = []
186
- @flags = Set.new
187
- @values = Hash.new
253
+ def add_default_value(name, value)
254
+ default_values.update(name => value)
188
255
  end
189
256
 
190
-
191
- def parse_arg(arg)
192
- # require "debug"; binding.break
193
- case arg
194
- when SINGLE_FLAG
195
- flags << Regexp.last_match[1]
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
257
+ def allow_value(value_def)
258
+ @allowed_values ||= Set.new
259
+ case value_def
260
+ in Symbol
261
+ @allowed_values << value_def
262
+ in [Symbol => value_name, default_value]
263
+ @allowed_values << value_name
264
+ add_default_value(value_name, default_value)
265
+ in _
266
+ raise ArgumentError, "arg must be of format Symbol or [Symbol, value], but was #{value_def}"
202
267
  end
203
268
  end
204
269
 
205
- def result
206
- OpenStruct.new(
207
- positionals: positionals,
208
- flags: flags,
209
- values: values,
210
- )
270
+ def define_alias(short, long)
271
+ alias_definitions.update(short => long)
211
272
  end
273
+
212
274
  end
213
275
  # 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.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Dober
@@ -31,9 +31,13 @@ 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
37
41
  - lib/flex_args/version.rb
38
42
  homepage: https://codeberg.org/lab419/flex_args
39
43
  licenses: