From ac6bbb8bf2d0388aa53d7bd2fd783043f57c2464 Mon Sep 17 00:00:00 2001 From: Oleh Fedorenko Date: Mon, 8 Apr 2019 13:17:40 +0000 Subject: [PATCH] Fixes #25878 - New lines in text attr dont break output --- doc/creating_commands.md | 10 +-- doc/developer_docs.md | 1 + doc/output.md | 43 +++++++++++ lib/hammer_cli/output/adapter/abstract.rb | 8 +-- lib/hammer_cli/output/adapter/base.rb | 4 +- lib/hammer_cli/output/adapter/csv.rb | 4 +- lib/hammer_cli/output/adapter/table.rb | 4 +- .../output/adapter/tree_structure.rb | 6 +- lib/hammer_cli/output/fields.rb | 3 + lib/hammer_cli/output/formatters.rb | 71 ++++++++++++------- test/unit/output/adapter/abstract_test.rb | 18 ++--- test/unit/output/formatters_test.rb | 55 ++++++++++++-- 12 files changed, 163 insertions(+), 64 deletions(-) create mode 100644 doc/output.md diff --git a/doc/creating_commands.md b/doc/creating_commands.md index c64c6e42..8a8ba8d5 100644 --- a/doc/creating_commands.md +++ b/doc/creating_commands.md @@ -442,13 +442,7 @@ directly with `puts` in Hammer. The reason is we separate definition of the output from its interpretation. Hammer uses so called _output adapters_ that can modify the output format. -Hammer comes with four basic output adapters: - * __base__ - simple output, structured records - * __table__ - records printed in tables, ideal for printing lists of records - * __csv__ - comma separated output, ideal for scripting and grepping - * __silent__ - no output, used for testing - -The detailed documentation on creating adapters is coming soon. +The detailed documentation on adapters and related things is [here](output.md#adapters). #### Printing messages Very simple, just call @@ -461,7 +455,7 @@ Typical usage of a CLI is interaction with some API. In many cases it's listing some records returned by the API. Hammer comes with support for selecting and formatting of hash record fields. -You first create a _output definition_ that you apply to your data. The result +You first create an _output definition_ that you apply to your data. The result is a collection of fields, each having its type. The collection is then passed to an _output adapter_ which handles the actual formatting and printing. diff --git a/doc/developer_docs.md b/doc/developer_docs.md index a618affa..7504e92a 100644 --- a/doc/developer_docs.md +++ b/doc/developer_docs.md @@ -8,6 +8,7 @@ before creating hammer specific plugins. Contents: - [Writing a plugin](writing_a_plugin.md#writing-your-own-hammer-plugin) - [Creating commands](creating_commands.md#create-your-first-command) + - [Output adapters, formatters, etc](output.md#output) - [Help modification](help_modification.md#modify-an-existing-help) - [Commands modification](commands_modification.md#modify-an-existing-command) - [Option builders](option_builders.md#option-builders) diff --git a/doc/output.md b/doc/output.md new file mode 100644 index 00000000..47ebe89a --- /dev/null +++ b/doc/output.md @@ -0,0 +1,43 @@ +Output +------------------------------ + +### Adapters +Output adapter is responsible for rendering the output in a specific way using +__formatters__ (see below). +Hammer comes with following adapters: + * __base__ - simple output, structured records + * __table__ - records printed in tables, ideal for printing lists of records + * __csv__ - comma separated output, ideal for scripting and grepping + * __yaml__ - YAML output + * __json__ - JSON output + * __silent__ - no output, used for testing + +### Formatters +Formatter is a bit of code that can modify a representation of a field during +output rendering. Formatter is registered for specific field type. Each field +type can have multiple formatters. + * __ColorFormatter__ - colors data with a specific color + * __DateFormatter__ - formats a date string with `%Y/%m/%d %H:%M:%S` style + * __ListFormatter__ - formats an array of data with csv style + * __KeyValueFormatter__ - formats a hash with `key => value` style + * __BooleanFormatter__ - converts `1/0`/`true/false`/`""` to `"yes"`/`"no"` + * __LongTextFormatter__ - adds a new line at the start of the data string + * __InlineTextFormatter__ - removes all new lines from data string + * __MultilineTextFormatter__ - splits a long data string to fixed size chunks + with indentation + +### Formatter features/Adapter limitations +Currently used formatter features (or adapter limitations) are of two kinds. +The first one help us to align by structure of the output: + * __:flat__ - means the fields are serialized into a string (__table__, __csv__, __base__ adapters) + * __:data__ - means the output is structured (__yaml__, __json__ adapters) + * __:inline__ - means that the value will be rendered into single line without newlines (__table__, __csv__ adapters) + * __:multiline__ - means that the properly indented value will be printed over multiple lines (__base__ adapter) + +The other kind serves to distinguish the cases where we can use the xterm colors +to improve the output: + * __:screen__ - means we can use the xterm colors (__table__, __base__ adapters) + * __:file__ - unused yet + +All the features the formatter has need to match (be present in) the adapter's +limitations. Otherwise the formatter won't apply. diff --git a/lib/hammer_cli/output/adapter/abstract.rb b/lib/hammer_cli/output/adapter/abstract.rb index e0c5fbcb..d65c5d1d 100644 --- a/lib/hammer_cli/output/adapter/abstract.rb +++ b/lib/hammer_cli/output/adapter/abstract.rb @@ -2,8 +2,8 @@ module HammerCLI::Output::Adapter class Abstract - def tags - [] + def limitations + %i[] end def initialize(context={}, formatters={}) @@ -88,9 +88,9 @@ def filter_formatters(formatters_map) formatters_map ||= {} formatters_map.inject({}) do |map, (type, formatter_list)| # remove incompatible formatters - filtered = formatter_list.select { |f| f.match?(tags) } + filtered = formatter_list.select { |f| f.match?(limitations) } # put serializers first - map[type] = filtered.sort_by { |f| f.tags.include?(:flat) ? 0 : 1 } + map[type] = filtered.sort_by { |f| f.features.include?(:flat) ? 0 : 1 } map end end diff --git a/lib/hammer_cli/output/adapter/base.rb b/lib/hammer_cli/output/adapter/base.rb index 5f760ea6..b07140e3 100644 --- a/lib/hammer_cli/output/adapter/base.rb +++ b/lib/hammer_cli/output/adapter/base.rb @@ -4,8 +4,8 @@ class Base < Abstract GROUP_INDENT = " "*4 LABEL_DIVIDER = ": " - def tags - [:flat, :screen] + def limitations + %i[flat screen multiline] end def print_record(fields, record) diff --git a/lib/hammer_cli/output/adapter/csv.rb b/lib/hammer_cli/output/adapter/csv.rb index e2bef566..316aa154 100644 --- a/lib/hammer_cli/output/adapter/csv.rb +++ b/lib/hammer_cli/output/adapter/csv.rb @@ -131,8 +131,8 @@ def initialize(context={}, formatters={}) @paginate_by_default = false end - def tags - [:flat] + def limitations + %i[flat inline] end def row_data(fields, collection) diff --git a/lib/hammer_cli/output/adapter/table.rb b/lib/hammer_cli/output/adapter/table.rb index 339ed493..af0732fa 100644 --- a/lib/hammer_cli/output/adapter/table.rb +++ b/lib/hammer_cli/output/adapter/table.rb @@ -12,8 +12,8 @@ class Table < Abstract LINE_SEPARATOR = '-|-' COLUMN_SEPARATOR = ' | ' - def tags - [:screen, :flat] + def limitations + %i[screen flat inline] end def print_record(fields, record) diff --git a/lib/hammer_cli/output/adapter/tree_structure.rb b/lib/hammer_cli/output/adapter/tree_structure.rb index 53c2b281..fbb248cd 100644 --- a/lib/hammer_cli/output/adapter/tree_structure.rb +++ b/lib/hammer_cli/output/adapter/tree_structure.rb @@ -6,10 +6,8 @@ def initialize(context={}, formatters={}) @paginate_by_default = false end - def tags - [ - :data - ] + def limitations + %i[data] end def prepare_collection(fields, collection) diff --git a/lib/hammer_cli/output/fields.rb b/lib/hammer_cli/output/fields.rb index f6408506..3b1312cd 100644 --- a/lib/hammer_cli/output/fields.rb +++ b/lib/hammer_cli/output/fields.rb @@ -89,6 +89,9 @@ class List < Field class LongText < Field end + class Text < Field + end + class KeyValue < Field end diff --git a/lib/hammer_cli/output/formatters.rb b/lib/hammer_cli/output/formatters.rb index 896b79ee..c42dbfc3 100644 --- a/lib/hammer_cli/output/formatters.rb +++ b/lib/hammer_cli/output/formatters.rb @@ -24,23 +24,15 @@ def formatter_for_type(type) end end - # Tags: - # All the tags the formatter has, needs to be present in the addapter. - # Otherwise the formatter won't apply. Formatters with :flat tag are used first - # as we expect them to serialize the value. - # - # - by format: :flat x :data - # - by output: :file X :screen - # abstract formatter class FieldFormatter - def tags - [] + def features + %i[] end - def match?(other_tags) - tags & other_tags == tags + def match?(limitations) + features & limitations == features end def format(data, field_params={}) @@ -69,8 +61,8 @@ def initialize(color) @color = color end - def tags - [:screen, :flat] + def features + %i[screen flat] end def format(data, field_params={}) @@ -80,8 +72,8 @@ def format(data, field_params={}) class DateFormatter < FieldFormatter - def tags - [:flat] + def features + %i[flat] end def format(string_date, field_params={}) @@ -95,8 +87,8 @@ def format(string_date, field_params={}) class ListFormatter < FieldFormatter INDENT = " " - def tags - [:flat] + def features + %i[flat] end def format(list, field_params={}) @@ -117,8 +109,8 @@ def format(list, field_params={}) class KeyValueFormatter < FieldFormatter - def tags - [:screen, :flat] + def features + %i[screen flat] end def format(params, field_params={}) @@ -140,8 +132,8 @@ def initialize(options = {}) @indent = options[:indent].nil? ? true : options[:indent] end - def tags - [:screen] + def features + %i[screen] end def format(text, field_params={}) @@ -150,10 +142,36 @@ def format(text, field_params={}) end end + class InlineTextFormatter < FieldFormatter + def features + %i[flat inline] + end + + def format(text, _field_params = {}) + text.to_s.tr("\r\n", ' ') + end + end + + class MultilineTextFormatter < FieldFormatter + INDENT = ' '.freeze + MAX_WIDTH = 120 + MIN_WIDTH = 60 + + def features + %i[flat multiline screen] + end + + def format(text, field_params = {}) + width = [[field_params.fetch(:width, 0), MIN_WIDTH].max, MAX_WIDTH].min + text.to_s.chars.each_slice(width).map(&:join).join("\n") + .indent_with(INDENT).prepend("\n") + end + end + class BooleanFormatter < FieldFormatter - def tags - [:flat, :screen] + def features + %i[flat screen] end def format(value, field_params={}) @@ -165,10 +183,9 @@ def format(value, field_params={}) HammerCLI::Output::Output.register_formatter(ListFormatter.new, :List) HammerCLI::Output::Output.register_formatter(KeyValueFormatter.new, :KeyValue) HammerCLI::Output::Output.register_formatter(LongTextFormatter.new, :LongText) + HammerCLI::Output::Output.register_formatter(InlineTextFormatter.new, :Text) + HammerCLI::Output::Output.register_formatter(MultilineTextFormatter.new, :Text) HammerCLI::Output::Output.register_formatter(BooleanFormatter.new, :Boolean) end end - - - diff --git a/test/unit/output/adapter/abstract_test.rb b/test/unit/output/adapter/abstract_test.rb index ea429b30..a5970b60 100644 --- a/test/unit/output/adapter/abstract_test.rb +++ b/test/unit/output/adapter/abstract_test.rb @@ -6,8 +6,8 @@ let(:adapter) { HammerCLI::Output::Adapter::Abstract.new } - it "should have tags" do - adapter.tags.must_be_kind_of Array + it "should have limitations" do + adapter.limitations.must_be_kind_of Array end class UnknownTestFormatter < HammerCLI::Output::Formatters::FieldFormatter @@ -15,8 +15,8 @@ def format(data, field_params={}) data+'.' end - def tags - [:unknown] + def features + %i[unknown] end end @@ -24,7 +24,7 @@ def tags adapter.paginate_by_default?.must_equal true end - it "should filter formatters with incompatible tags" do + it "should filter formatters with incompatible features" do HammerCLI::Output::Formatters::FormatterLibrary.expects(:new).with({ :type => [] }) adapter = adapter_class.new({}, {:type => [UnknownTestFormatter.new]}) @@ -34,18 +34,18 @@ def tags formatter = UnknownTestFormatter.new HammerCLI::Output::Formatters::FormatterLibrary.expects(:new).with({ :type => [formatter] }) # set :unknown tag to abstract - adapter_class.any_instance.stubs(:tags).returns([:unknown]) + adapter_class.any_instance.stubs(:limitations).returns([:unknown]) adapter = adapter_class.new({}, {:type => [formatter]}) end it "should put serializers first" do formatter1 = UnknownTestFormatter.new - formatter1.stubs(:tags).returns([]) + formatter1.stubs(:features).returns([]) formatter2 = UnknownTestFormatter.new - formatter2.stubs(:tags).returns([:flat]) + formatter2.stubs(:features).returns([:flat]) HammerCLI::Output::Formatters::FormatterLibrary.expects(:new).with({ :type => [formatter2, formatter1] }) # set :unknown tag to abstract - adapter_class.any_instance.stubs(:tags).returns([:flat]) + adapter_class.any_instance.stubs(:limitations).returns([:flat]) adapter = adapter_class.new({}, {:type => [formatter1, formatter2]}) end diff --git a/test/unit/output/formatters_test.rb b/test/unit/output/formatters_test.rb index 4abe6e95..752b620c 100644 --- a/test/unit/output/formatters_test.rb +++ b/test/unit/output/formatters_test.rb @@ -20,14 +20,14 @@ formatter.respond_to?(:format).must_equal true end - it "has tags" do - formatter.tags.must_be_kind_of Array + it "has features" do + formatter.features.must_be_kind_of Array end - it "should know if it has matching tags" do - formatter.stubs(:tags).returns([:tag]) - formatter.match?([:tag]).must_equal true - formatter.match?([:notag]).must_equal false + it "should know if it has matching features" do + formatter.stubs(:features).returns([:feature]) + formatter.match?([:feature]).must_equal true + formatter.match?([:nofeature]).must_equal false end end @@ -156,6 +156,49 @@ def format(data, field_params={}) end +describe HammerCLI::Output::Formatters::InlineTextFormatter do + let(:formatter) { HammerCLI::Output::Formatters::InlineTextFormatter.new } + + it 'prints multiline text to one line' do + formatter.format("Some\nmultiline\ntext").must_equal('Some multiline text') + end + + it 'accepts nil' do + formatter.format(nil).must_equal('') + end +end + +describe HammerCLI::Output::Formatters::MultilineTextFormatter do + let(:formatter) { HammerCLI::Output::Formatters::MultilineTextFormatter.new } + let(:multiline_text) { "Some\nmultiline\ntext" } + let(:indentation) { "\n " } + let(:long_multiline_text) { 'Lorem ipsum dolor' * 5 } + + it 'prints multiline text' do + formatter.format(multiline_text).must_equal( + "#{indentation}Some#{indentation}multiline#{indentation}text" + ) + end + + it 'accepts nil' do + formatter.format(nil).must_equal(indentation) + end + + it 'accepts field width param' do + formatter.format(long_multiline_text, width: 80) + .must_equal(indentation + long_multiline_text[0..-6] + + indentation + long_multiline_text[-5..-1]) + end + + it 'deals with strange params' do + formatter.format(long_multiline_text, width: -1) + .must_equal(indentation + long_multiline_text[0..-26] + + indentation + long_multiline_text[-25..-1]) + formatter.format(long_multiline_text, width: 999) + .must_equal(indentation + long_multiline_text) + end +end + describe HammerCLI::Output::Formatters::BooleanFormatter do let(:formatter) { HammerCLI::Output::Formatters::BooleanFormatter.new }