Skip to content

Commit

Permalink
Add arity check to options parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrmurach committed Apr 11, 2020
1 parent 0e9f492 commit 70e371f
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 21 deletions.
76 changes: 55 additions & 21 deletions lib/tty/option/parser/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ def initialize(options, **config)
@remaining = []
@shorts = {}
@longs = {}
@arities = {}
@arities = Hash.new(0)
@required = []
@multiplies = {}

setup_opts
end
Expand All @@ -40,6 +41,7 @@ def setup_opts
@options.each do |opt|
@shorts[opt.short_name] = opt
@longs[opt.long_name] = opt
@multiplies[opt.name] = opt if opt.multiple?

if opt.default?
case opt.default
Expand Down Expand Up @@ -68,7 +70,7 @@ def parse(argv)
opt, value = next_option
break if opt.nil?
@required.delete(opt)
@arities[opt.name] = @arities[opt.name].to_i + 1
@arities[opt.name] += 1

if block_given?
yield(opt, value)
Expand All @@ -77,30 +79,12 @@ def parse(argv)
end
end

check_arity
check_required

[@parsed, @remaining, @errors]
end

# Check if required options are provided
#
# @raise [MissingParameter]
#
# @api private
def check_required
return if @required.empty?

@required.each do |opt|
name = if opt.respond_to?(:long_name)
opt.long? ? opt.long_name : opt.short_name
else
opt.name
end
record_error(MissingParameter,
"need to provide #{name} #{opt.to_sym}", opt)
end
end

private

# Get next option
Expand Down Expand Up @@ -298,6 +282,56 @@ def assign_option(opt, val)
@parsed[opt.name] = value
end
end

# Check if parameter matches arity
#
# @raise [InvalidArity]
#
# @api private
def check_arity
@multiplies.each do |name, param|
arity = @arities[name]

if 0 < param.arity.abs && arity < param.arity.abs
prefix = param.arity < 0 ? "at least " : ""
expected_arity = param.arity < 0 ? param.arity.abs - 1 : param.arity

record_error(InvalidArity, format(
"expected %s %s to appear %s but appeared %s",
param.to_sym,
name.inspect,
prefix + pluralize("time", expected_arity),
pluralize("time", arity)
), param)
end
end
end

# Pluralize a noun
#
# @api private
def pluralize(noun, count = 1)
"#{count} #{noun}#{'s' unless count == 1}"
end

# Check if required options are provided
#
# @raise [MissingParameter]
#
# @api private
def check_required
return if @required.empty?

@required.each do |opt|
name = if opt.respond_to?(:long_name)
opt.long? ? opt.long_name : opt.short_name
else
opt.name
end
record_error(MissingParameter,
"need to provide #{name} #{opt.to_sym}", opt)
end
end
end # Options
end # Parser
end # Option
Expand Down
37 changes: 37 additions & 0 deletions spec/unit/parser/options_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -355,13 +355,50 @@ def parse(argv, options, **config)
expect(rest).to eq(%w[-f 3])
end

it "parses short flag with required argument many times and keeps only two" do
opt = option(:foo, short: "-f int", convert: :int_map, arity: 2)
params, rest = parse(%w[-f a:1 -f b:2 -f c:3], opt)

expect(params[:foo]).to eq({a: 1, b: 2})
expect(rest).to eq(["-f", {c: 3}])
end

it "parses short flag with required argument many times and keeps all" do
params, rest = parse(%w[-f 1 -f 2 -f 3], option(:foo, short: "-f int", arity: :any))

expect(params[:foo]).to eq(%w[1 2 3])
expect(rest).to eq([])
end

it "fails to match required arity for short flag" do
expect {
parse(%w[-f 1], option(:foo, short: "-f int", arity: 2))
}.to raise_error(TTY::Option::InvalidArity,
"expected option :foo to appear 2 times but appeared 1 time")
end

it "doesn't find enought options to match at least arity for short flag" do
expect {
parse(%w[-f 1], option(:foo, short: "-f int", arity: -3))
}.to raise_error(TTY::Option::InvalidArity,
"expected option :foo to appear at least 2 times but " \
"appeared 1 time")
end

it "collects all arity errors" do
options = []
options << option(:foo, short: "-f int", arity: 2)
options << option(:bar, short: "-b int", arity: -3)

params, rest, errors = parse(%w[-f 1 -b 2], options, raise_if_missing: false)

expect(params[:foo]).to eq(["1"])
expect(params[:bar]).to eq(["2"])
expect(rest).to eq([])
expect(errors[:foo]).to eq({invalid_arity: "expected option :foo to appear 2 times but appeared 1 time"})
expect(errors[:bar]).to eq({invalid_arity: "expected option :bar to appear at least 2 times but appeared 1 time"})
end

it "parses long flag with required argument and keeps the last argument" do
params, rest = parse(%w[--f 1 --f 2 --f 3], option(:foo, long: "--f int"))

Expand Down

0 comments on commit 70e371f

Please sign in to comment.