Skip to content

Commit

Permalink
Fixes #25878 - New lines in text attr dont break output
Browse files Browse the repository at this point in the history
  • Loading branch information
ofedoren committed Apr 8, 2019
1 parent 162d13b commit ac6bbb8
Show file tree
Hide file tree
Showing 12 changed files with 163 additions and 64 deletions.
10 changes: 2 additions & 8 deletions doc/creating_commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down
1 change: 1 addition & 0 deletions doc/developer_docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
43 changes: 43 additions & 0 deletions doc/output.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 4 additions & 4 deletions lib/hammer_cli/output/adapter/abstract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ module HammerCLI::Output::Adapter

class Abstract

def tags
[]
def limitations
%i[]
end

def initialize(context={}, formatters={})
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/hammer_cli/output/adapter/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions lib/hammer_cli/output/adapter/csv.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions lib/hammer_cli/output/adapter/table.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 2 additions & 4 deletions lib/hammer_cli/output/adapter/tree_structure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions lib/hammer_cli/output/fields.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ class List < Field
class LongText < Field
end

class Text < Field
end

class KeyValue < Field
end

Expand Down
71 changes: 44 additions & 27 deletions lib/hammer_cli/output/formatters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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={})
Expand Down Expand Up @@ -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={})
Expand All @@ -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={})
Expand All @@ -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={})
Expand All @@ -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={})
Expand All @@ -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={})
Expand All @@ -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={})
Expand All @@ -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



18 changes: 9 additions & 9 deletions test/unit/output/adapter/abstract_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,25 @@
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
def format(data, field_params={})
data+'.'
end

def tags
[:unknown]
def features
%i[unknown]
end
end

it "allows default pagination" do
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]})
Expand All @@ -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

Expand Down
Loading

0 comments on commit ac6bbb8

Please sign in to comment.