Skip to content

Commit

Permalink
Fixes #26120 - Extended capabilities for list type options
Browse files Browse the repository at this point in the history
  • Loading branch information
ofedoren authored and mbacovsky committed Apr 24, 2019
1 parent f9c0e12 commit 494ae8e
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 11 deletions.
19 changes: 19 additions & 0 deletions lib/hammer_cli/abstract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ def inherited_command_extensions
end
extensions
end

def extend_options_help(option)
extend_help do |h|
begin
h.find_item(:s_option_details)
rescue ArgumentError
option_details = HammerCLI::Help::Section.new(_('Option details'), nil, id: :s_option_details, richtext: true)
option_details.definition << HammerCLI::Help::Text.new(
_('Following parameters accept format defined by its schema (bold are required):')
)
h.definition.unshift(option_details)
ensure
h.find_item(:s_option_details).definition << HammerCLI::Help::List.new([
[option.switches.last, option.value_formatter.schema.description]
])
end
end
end
end

def adapter
Expand Down Expand Up @@ -169,6 +187,7 @@ def self.build_options(builder_params={})
declared_options << option
block ||= option.default_conversion_block
define_accessors_for(option, &block)
extend_options_help(option) if option.value_formatter.is_a?(HammerCLI::Options::Normalizers::ListNested)
end
end

Expand Down
6 changes: 5 additions & 1 deletion lib/hammer_cli/apipie/option_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ def option_opts(param)
opts = {}
opts[:required] = true if (param.required? and require_options?)
if param.expected_type.to_s == 'array'
opts[:format] = HammerCLI::Options::Normalizers::List.new
if param.params.empty?
opts[:format] = HammerCLI::Options::Normalizers::List.new
else
opts[:format] = HammerCLI::Options::Normalizers::ListNested.new(param.params)
end
elsif param.expected_type.to_s == 'boolean' || param.validator.to_s == 'boolean'
opts[:format] = HammerCLI::Options::Normalizers::Bool.new
elsif param.expected_type.to_s == 'string' && param.validator =~ /Must be one of: (.*)\./
Expand Down
60 changes: 55 additions & 5 deletions lib/hammer_cli/options/normalizers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ def format(value)

class KeyValueList < AbstractNormalizer

PAIR_RE = '([^,=]+)=([^,\[]+|\[[^\[\]]*\])'
PAIR_RE = '([^,=]+)=([^,\{\[]+|[\{\[][^\{\}\[\]]*[\}\]])'
FULL_RE = "^((%s)[,]?)+$" % PAIR_RE

def description
_("Comma-separated list of key=value")
_("Comma-separated list of key=value.") + "\n" +
_("JSON is acceptable and preferred way for complex parameters")
end

def format(val)
Expand Down Expand Up @@ -60,7 +61,11 @@ def parse_key_value(val)
result = {}
val.scan(Regexp.new(PAIR_RE)) do |key, value|
value = value.strip
value = value.scan(/[^,\[\]]+/) if value.start_with?('[')
if value.start_with?('[')
value = value.scan(/[^,\[\]]+/)
elsif value.start_with?('{')
value = parse_key_value(value[1...-1])
end

result[key.strip] = strip_value(value)
end
Expand All @@ -72,6 +77,10 @@ def strip_value(value)
value.map do |item|
strip_chars(item.strip, '"\'')
end
elsif value.is_a? Hash
value.map do |key, val|
[strip_chars(key.strip, '"\''), strip_chars(val.strip, '"\'')]
end.to_h
else
strip_chars(value.strip, '"\'')
end
Expand All @@ -86,14 +95,55 @@ def strip_chars(string, chars)

class List < AbstractNormalizer
def description
_("Comma separated list of values. Values containing comma should be quoted or escaped with backslash")
_("Comma separated list of values. Values containing comma should be quoted or escaped with backslash.") + "\n" +
_("JSON is acceptable and preferred way for complex parameters")
end

def format(val)
(val.is_a?(String) && !val.empty?) ? HammerCLI::CSVParser.new.parse(val) : []
return [] unless val.is_a?(String) && !val.empty?
begin
JSON.parse(val)
rescue JSON::ParserError
HammerCLI::CSVParser.new.parse(val)
end
end
end

class ListNested < AbstractNormalizer
class Schema < Array
def description
'"' + reduce([]) do |schema, nested_param|
name = nested_param.name
name = HighLine.color(name, :bold) if nested_param.required?
schema << "#{name}=#{nested_param.expected_type}"
end.join('\,').concat(', ... "')
end
end

attr_reader :schema

def initialize(schema)
@schema = Schema.new(schema)
end

def description
_("Comma separated list of values defined by a schema. See Option details section below.") + "\n" +
_("JSON is acceptable and preferred way for complex parameters")
end

def format(val)
return [] unless val.is_a?(String) && !val.empty?
begin
JSON.parse(val)
rescue JSON::ParserError
HammerCLI::CSVParser.new.parse(val).inject([]) do |results, item|
next if item.empty?

results << KeyValueList.new.format(item)
end
end
end
end

class Number < AbstractNormalizer

Expand Down
97 changes: 92 additions & 5 deletions test/unit/options/normalizers_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@
end

describe 'default' do

let(:formatter) { HammerCLI::Options::Normalizers::Default.new }

it "should not change any value" do
formatter.format('value').must_equal 'value'
end

it "should not change nil value" do
formatter.format(nil).must_be_nil
end

it "has empty description" do
formatter.description.must_equal ''
end

it "has empty completion" do
formatter.complete('test').must_equal []
end
Expand Down Expand Up @@ -69,8 +69,85 @@
it "should catch quoting errors" do
proc { formatter.format('1,"3,4""s') }.must_raise ArgumentError
end

it "should accept and parse JSON" do
formatter.format("{\"name\":\"bla\", \"value\":1}").must_equal(
JSON.parse("{\"name\":\"bla\", \"value\":1}")
)
end
end

describe 'list_nested' do
let(:params_raw) do
[
{name: 'name', expected_type: :string, validator: 'string', description: ''},
{name: 'value', expected_type: :string, validator: 'string', description: ''}
]
end
let(:params) do
[
ApipieBindings::Param.new(params_raw.first),
ApipieBindings::Param.new(params_raw.last)
]
end
let(:param) do
ApipieBindings::Param.new({
name: 'array', expected_type: :array, validator: 'nested', description: '',
params: params_raw
})
end
let(:formatter) { HammerCLI::Options::Normalizers::ListNested.new(param.params) }

it "should accept and parse JSON" do
formatter.format("{\"name\":\"bla\", \"value\":1}").must_equal(
JSON.parse("{\"name\":\"bla\", \"value\":1}")
)
end

it "should parse simple input" do
formatter.format("name=test\\,value=1,name=other\\,value=2").must_equal(
[{'name' => 'test', 'value' => '1'}, {'name' => 'other', 'value' => '2'}]
)
end

it "should parse unexpected input" do
formatter.format("name=test\\,value=1,name=other\\,value=2,unexp=doe").must_equal(
[
{'name' => 'test', 'value' => '1'}, {'name' => 'other', 'value' => '2'},
{'unexp' => 'doe'}
]
)
end

it "should accept arrays" do
formatter.format("name=test\\,value=1,name=other\\,value=[1\\,2\\,3]").must_equal(
[{'name' => 'test', 'value' => '1'}, {'name' => 'other', 'value' => ['1', '2', '3']}]
)
end

it "should accept hashes" do
formatter.format(
"name=test\\,value={key=key1\\,value=1},name=other\\,value={key=key2\\,value=2}"
).must_equal(
[
{'name' => 'test', 'value' => {'key' => 'key1', 'value' => '1'}},
{'name' => 'other', 'value' => {'key' => 'key2', 'value' => '2'}},
]
)
end

it "should accept combined input" do
formatter.format(
"name=foo\\,value=1\\,adds=[1\\,2\\,3]\\,cpu={name=ddd\\,type=abc}," \
"name=bar\\,value=2\\,adds=[2\\,2\\,2]\\,cpu={name=ccc\\,type=cba}"
).must_equal(
[
{'name' => 'foo', 'value' => '1', 'adds' => ['1','2','3'], 'cpu' => {'name' => 'ddd', 'type' => 'abc'}},
{'name' => 'bar', 'value' => '2', 'adds' => ['2','2','2'], 'cpu' => {'name' => 'ccc', 'type' => 'cba'}}
]
)
end
end

describe 'key_value_list' do

Expand Down Expand Up @@ -133,6 +210,16 @@
formatter.format("a=1,b=[],c=3").must_equal({'a' => '1', 'b' => [], 'c' => '3'})
end

it "should parse hash with one item" do
formatter.format("a=1,b={key=abc,value=abc},c=3").must_equal(
{'a' => '1', 'b' => {'key' => 'abc', 'value' => 'abc'}, 'c' => '3'}
)
end

it "should parse empty hash" do
formatter.format("a=1,b={},c=3").must_equal({'a' => '1', 'b' => {}, 'c' => '3'})
end

it "should parse a comma separated string 2" do
proc { formatter.format("a=1,b,c=3") }.must_raise ArgumentError
end
Expand Down

0 comments on commit 494ae8e

Please sign in to comment.