output_mode 1.4.0 → 1.5.0

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: 2ad0728b37cf092a7e009021655552acae2f4493b66bafa17509efc0e0740435
4
- data.tar.gz: 96875c08fa06eb7b868e7d4069965d6704e75ac126917fba5fb7e0f78cf4ff2e
3
+ metadata.gz: 33db72e9e5872a974df80e11a0d4bae3735c13d49d3be4d4ab1366b7e461551f
4
+ data.tar.gz: f1ac7923d6cb9beda1f3af36ebaa41b84cdb6567d5fb71db4f2d1a2503ee731b
5
5
  SHA512:
6
- metadata.gz: 8b104681823f3d151875c4ebfef4eac7c98f64657fe0f40fb49776bd969095da7310dbe7d3119a49e92c758e71521d8b84de52636766efdfbae4c8e5b3a19b78
7
- data.tar.gz: 44f52488f5131c0c82df6ef4a272f75dd01f43c97c0c9e04c77f6e7df79968b04ef10af3d6ff2acc526fb7796782ab38b0ad7acc336de2868ff992314d70e4ac
6
+ metadata.gz: f12f87485cdad45ceb4fba9b9b26c405366009b1ad39e87c223fc246424bd0501421a75570caf48c351523d55483f4ea1d705659a3e6185ae8213c6147d9ad96
7
+ data.tar.gz: cafd82a58648d5e4900c91d242a1128b2b1c5d1123716385160801a3d85959324df691ecd7c8443e038612e4c2b2afb221745da598483e239a15a7ef9a701c0a
data/Gemfile CHANGED
@@ -28,3 +28,10 @@ source "https://rubygems.org"
28
28
 
29
29
  # Specify your gem's dependencies in output_mode.gemspec
30
30
  gemspec
31
+
32
+ # NOTE: Checks out the openflight version of tty-table with the 'rotate' flag
33
+ # fix. This should eventually become part of the mainline version of TTY::Table
34
+ #
35
+ # This is intentionally not part of the gemspec so older versions of TTY::Table
36
+ # can still be used
37
+ gem 'tty-table', github: 'openflighthpc/tty-table', branch: '9b326fcbe04968463da58c000fbb1dd5ce178243'
data/bin/demo CHANGED
@@ -36,9 +36,22 @@ module DemoIndex
36
36
  register_callable(header: 'Standard', header_color: [:strikethrough] ) { 'always visible' }
37
37
  register_callable(header: 'Verbose', verbose: true) { 'verbose visible' }
38
38
  register_callable(header: 'Simplified', verbose: false) { 'simplified visible' }
39
+ register_callable(header: 'Interactive', interactive: true) { 'interactive visible' }
40
+ register_callable(header: 'Non Interactive', interactive: false) { 'non-interactive visible' }
39
41
  register_callable(header: 'Yes/True') { true }
40
42
  register_callable(header: 'No/False', row_color: [:clear]) { false }
41
43
  register_callable(header: 'Missing') { nil }
44
+ register_callable(header: 'Inline') do |interactive:, verbose:|
45
+ if interactive && verbose
46
+ 'interactive-verbose'
47
+ elsif interactive
48
+ 'interactive-simplified'
49
+ elsif verbose
50
+ 'non-interactive-verbose'
51
+ else
52
+ 'non-interactive-simplified'
53
+ end
54
+ end
42
55
  end
43
56
 
44
57
  module DemoShow
@@ -48,9 +61,13 @@ module DemoShow
48
61
  register_callable(header: 'Standard') { 'always visible' }
49
62
  register_callable(header: 'Verbose', verbose: true) { 'verbose visible' }
50
63
  register_callable(header: 'Simplified', verbose: false) { 'simplified visible' }
64
+ register_callable(header: 'Interactive', interactive: true) { 'interactive visible' }
65
+ register_callable(header: 'Non Interactive', interactive: false) { 'non-interactive visible' }
51
66
  register_callable(header: 'Yes/True', section: :boolean) { true }
52
67
  register_callable(header: 'No/False', section: :boolean) { false }
53
68
  register_callable(header: 'Missing') { nil }
69
+ register_callable(header: 'Tab') { "tab1\ttab2" }
70
+ register_callable(header: 'New line') { "line1\nline2" }
54
71
  end
55
72
 
56
73
  data = [1, 2, 3]
@@ -36,7 +36,9 @@ module OutputMode
36
36
  # @param config Directly provided to {OutputMode::Callable#initialize}
37
37
  # @yield Directly provided to {OutputMode::Callable#initialize}
38
38
  def register_callable(**config, &b)
39
- output_callables << Callable.new(**config, &b)
39
+ Callable.new(**config, &b).tap do |c|
40
+ output_callables << c
41
+ end
40
42
  end
41
43
 
42
44
  # Provides the base method signature
@@ -26,16 +26,17 @@
26
26
 
27
27
  module OutputMode
28
28
  # Internal array like object that will convert procs to Callable
29
- class Callables < Array
29
+ class Callables
30
+ include Enumerable
31
+
30
32
  # @api private
31
33
  def initialize(callables = nil)
34
+ @callables = []
32
35
  case callables
33
- when Array
34
- super().tap do |all|
35
- callables.each { |c| all << c }
36
- end
36
+ when Array, Callables
37
+ callables.each { |c| @callables << c }
37
38
  when nil
38
- super()
39
+ # NOOP
39
40
  else
40
41
  raise "Can not convert #{callables.class} into a #{self.class}"
41
42
  end
@@ -43,13 +44,45 @@ module OutputMode
43
44
 
44
45
  def <<(item)
45
46
  if item.is_a? Callable
46
- super
47
+ @callables << item
47
48
  elsif item.respond_to?(:call)
48
- super(Callable.new(&item))
49
+ @callables << Callable.new(&item)
49
50
  else
50
51
  raise Error, "#{item.class} is not callable"
51
52
  end
52
53
  end
54
+
55
+ def each(&block)
56
+ @callables.each(&block)
57
+ end
58
+
59
+ def pad_each(key = :header)
60
+ max_length = self.map { |c| c.config[key].to_s.length }
61
+ .max
62
+
63
+ pads = self.map do |callable|
64
+ length = max_length - callable.config[key].to_s.length
65
+ [callable, { padding: ' ' * length }]
66
+ end
67
+
68
+ if block_given?
69
+ pads.each { |*args| yield(*args) }
70
+ else
71
+ pads.each
72
+ end
73
+ end
74
+
75
+ def config_select(key, *values)
76
+ selected = self.select do |callable|
77
+ conf = callable.config[key]
78
+ if conf.is_a? Array
79
+ !(conf & values).empty?
80
+ else
81
+ values.include?(conf)
82
+ end
83
+ end
84
+ Callables.new(selected)
85
+ end
53
86
  end
54
87
 
55
88
  class Callable
@@ -141,6 +174,29 @@ module OutputMode
141
174
  def call(*a)
142
175
  callable.call(*a)
143
176
  end
177
+
178
+ def generator(output)
179
+ ->(*a) do
180
+ # Implicitly determine which parts of the context can be passed through
181
+ ctx = if callable.parameters.any? { |type, _| type == :keyrest }
182
+ output.context
183
+ else
184
+ keys = callable.parameters.select { |type, _| [:key, :keyreq].include?(type) }
185
+ .map { |_, k| k }
186
+ output.context.slice(*keys)
187
+ end
188
+ raw = call(*a, **ctx)
189
+ if raw == true
190
+ config[:yes] || output.yes
191
+ elsif raw == false
192
+ config[:no] || output.no
193
+ elsif [nil, ''].include?(raw)
194
+ config[:default] || output.default
195
+ else
196
+ raw
197
+ end
198
+ end
199
+ end
144
200
  end
145
201
  end
146
202
 
@@ -36,45 +36,41 @@ module OutputMode
36
36
  # @!attribute [r] config
37
37
  # @return [Hash] additional key-values to modify the render
38
38
  # @!attribute [r] default
39
- # @return either a static default or a column based array of defaults
39
+ # @return either a static default
40
40
  # @!attribute [r] yes
41
- # @return either a static yes value or a column based array of values
41
+ # @return either a static yes value
42
42
  # @!attribute [r] no
43
- # @return either a static no value or a column based array of values
44
- attr_reader :procs, :config, :yes, :no, :default
43
+ # @return either a static no value
44
+ # @!attribute [r] context
45
+ # @return a hash of keys to be provided to the callables
46
+ attr_reader :procs, :config, :yes, :no, :default, :context
45
47
 
46
48
  # Creates a new outputting instance from an array of procs
47
49
  #
48
50
  # @param *procs [Array<#call>] an array of procs (or callable objects)
49
51
  # @param default: [String] replaces _blanks_ with a static string
50
- # @param default: [Array] replace _blanks_ on a per column basis. The last value is repeated if the +procs+ are longer.
51
52
  # @param yes: [String] replaces +true+ with a static string
52
- # @param yes: [Array] replaces +true+ on a per column basis. The last value is repeated if the +procs+ are longer.
53
53
  # @param no: [String] replaces +false+ with a static string
54
- # @param no: [Array] replaces +false+ on a per column basis. The last value is repeated if the +procs+ are longer.
54
+ # @param context: [Hash] of keys to be provided to the callables
55
55
  # @param **config [Hash] a hash of additional keys to be stored
56
- def initialize(*procs, default: nil, yes: 'true', no: 'false', **config)
57
- @procs = procs
56
+ def initialize(*procs, default: nil, yes: 'true', no: 'false', context: {}, **config)
57
+ @procs = Callables.new(procs)
58
58
  @config = config
59
59
  @yes = yes
60
60
  @no = no
61
61
  @default = default
62
+ @context = context
63
+ end
64
+
65
+ def callables
66
+ procs
62
67
  end
63
68
 
64
69
  # Returns the results of the +procs+ for a particular +object+. It will apply the
65
70
  # +default+, +yes+, and +no+ values.
66
71
  def generate(object)
67
- procs.each_with_index.map do |p, idx|
68
- raw = p.call(object)
69
- if raw == true
70
- index_selector(:yes, idx)
71
- elsif raw == false
72
- index_selector(:no, idx)
73
- elsif !default.nil? && (raw.nil? || raw == '')
74
- index_selector(:default, idx)
75
- else
76
- raw
77
- end
72
+ procs.map do |callable|
73
+ callable.generator(self).call(object)
78
74
  end
79
75
  end
80
76
 
@@ -91,37 +87,5 @@ module OutputMode
91
87
  def render(*data)
92
88
  raise NotImplementedError
93
89
  end
94
-
95
- # A helper method for selecting elements from a source array or return
96
- # a static value.
97
- #
98
- # @param [Symbol] method The source method on the +output+
99
- # @param [Integer] index The index to lookup
100
- #
101
- # @overload index_selector(array_method, valid_index)
102
- # @param array_method A method that returns an array
103
- # @param valid_index An index that is less than the array's length
104
- # @return the value at the index
105
- #
106
- # @overload index_selector(array_method, out_of_bounds)
107
- # @param array_method A method that returns an array
108
- # @param out_of_bounds An index greater than the maximum array length
109
- # @return the last element of the array
110
- #
111
- # @overload index_selector(non_array_method, _)
112
- # @param non_array_method A method that does not return an array
113
- # @param _ The index is ignored
114
- # @return the result of the non_array_method
115
- def index_selector(method, index)
116
- source = public_send(method)
117
- is_array = source.is_a? Array
118
- if is_array && source.length > index
119
- source[index]
120
- elsif is_array
121
- source.last
122
- else
123
- source
124
- end
125
- end
126
90
  end
127
91
  end
@@ -40,7 +40,12 @@ module OutputMode
40
40
  def render(*data)
41
41
  io = StringIO.new
42
42
  csv = CSV.new(io, **config)
43
- data.each { |d| csv << generate(d) }
43
+ data.each do |datum|
44
+ csv << generate(datum).map do |value|
45
+ next nil if value.nil?
46
+ value.to_s.dump[1...-1]
47
+ end
48
+ end
44
49
  io.tap(&:rewind).read
45
50
  end
46
51
  end
@@ -31,8 +31,6 @@ module OutputMode
31
31
  class Tabulated < Output
32
32
  # @!attribute [r] renderer
33
33
  # @return [Symbol] the renderer type, see: https://github.com/piotrmurach/tty-table#32-renderer
34
- # @!attribute [r] header
35
- # @return [Array] An optional header row for the table
36
34
  # @!attribute [r] block
37
35
  # @return [#call] an optional block of code that configures the renderer
38
36
  # @!attribute [r] colorize
@@ -41,28 +39,25 @@ module OutputMode
41
39
  # @return An optional header color or array of colors
42
40
  # @!attribute [r] row_color
43
41
  # @return An optional data color or array of colors
44
- attr_reader :renderer, :header, :default, :block, :yes, :no,
42
+ attr_reader :renderer, :default, :block, :yes, :no,
45
43
  :header_color, :row_color, :colorize
46
44
 
47
45
  # @return [Hash] additional options to +TTY::Table+ renderer
48
46
  # @see https://github.com/piotrmurach/tty-table#33-options
49
47
  def config; super; end
50
48
 
51
- # @overload initialize(*procs, renderer: nil, header: nil, **config)
49
+ # @overload initialize(*procs, renderer: nil, **config)
52
50
  # @param [Array] *procs see {OutputMode::Outputs::Base#initialize}
53
51
  # @param [Symbol] :renderer override the default renderer
54
- # @param [Array<String>] :header the header row of the table
55
52
  # @param [Hash] **config additional options to the renderer
56
53
  # @yieldparam tty_table_renderer [TTY::Table::Renderer::Base] optional access the underlining TTY::Table renderer
57
54
  def initialize(*procs,
58
55
  renderer: :unicode,
59
56
  colorize: false,
60
- header: nil,
61
57
  header_color: nil,
62
58
  row_color: nil,
63
59
  **config,
64
60
  &block)
65
- @header = header
66
61
  @renderer = renderer
67
62
  @block = block
68
63
  @header_color = header_color
@@ -76,7 +71,7 @@ module OutputMode
76
71
  # @see https://github.com/piotrmurach/tty-table
77
72
  def render(*data)
78
73
  table = TTY::Table.new header: processed_header
79
- data.each { |d| table << process_row(generate(d)) }
74
+ data.each { |d| table << process_row(d) }
80
75
  table.render(renderer, **config, &block) || ''
81
76
  end
82
77
 
@@ -84,23 +79,25 @@ module OutputMode
84
79
 
85
80
  # Colorizes the header when requested
86
81
  def processed_header
87
- header&.each_with_index&.map do |h, idx|
88
- color = index_selector(:header_color, idx)
82
+ callables.map do |callable|
83
+ header = callable.config.fetch(:header, '')
84
+ color = callable.config.fetch(:header_color) || header_color
89
85
  case color
90
86
  when nil
91
- h.to_s
87
+ header.to_s
92
88
  when Array
93
- pastel.decorate(h.to_s, *color)
89
+ pastel.decorate(header.to_s, *color)
94
90
  else
95
- pastel.decorate(h.to_s, color)
91
+ pastel.decorate(header.to_s, color)
96
92
  end
97
93
  end
98
94
  end
99
95
 
100
96
  # Colorizes the row when requested
101
- def process_row(data)
102
- data.each_with_index.map do |d, idx|
103
- color = index_selector(:row_color, idx)
97
+ def process_row(model)
98
+ callables.map do |callable|
99
+ d = callable.generator(self).call(model)
100
+ color = callable.config[:row_color] || row_color
104
101
  case color
105
102
  when NilClass
106
103
  d.to_s
@@ -38,27 +38,25 @@ module OutputMode
38
38
  # @yieldparam field: An optional field header for the value
39
39
  # @yieldparam padding: A padding string which will right align the +field+
40
40
  # @yieldparam **config TBA
41
- def each(section = nil)
42
- # Select the indices for the relevant section
43
- indices = (0...output.procs.length).to_a
44
- if section
45
- indices.select! do |idx|
46
- output.index_selector(:sections, idx) == section
47
- end
48
- end
49
-
50
- # Find the max field length
51
- max = indices.map do |idx|
52
- output.index_selector(:fields, idx).to_s.length
53
- end.max
41
+ def each(section = nil, &block)
42
+ # Select the callable objects
43
+ callables = if section == nil
44
+ output.callables
45
+ elsif section == :default
46
+ output.callables.config_select(:section, :default, nil)
47
+ else
48
+ output.callables.config_select(:section, section)
49
+ end
54
50
 
55
51
  # Yield each selected attribute
56
- indices.each do |idx|
57
- value = generated[idx]
58
- field = output.index_selector(:fields, idx)
59
- padding = ' ' * (max - field.to_s.length)
60
- yield(value, field: field, padding: padding)
52
+ objs = callables.pad_each.map do |callable, padding:|
53
+ value = callable.generator(output).call(model)
54
+ field = callable.config[:header]
55
+ [value, {field: field, padding: padding }]
61
56
  end
57
+
58
+ # Runs the provided block
59
+ objs.each(&block)
62
60
  end
63
61
 
64
62
  # Renders an ERB object within the entry's context. This provides access to the
@@ -35,12 +35,18 @@ module OutputMode
35
35
  # @overload register_callable(header:, verbose: true)
36
36
  # @param header: The column's header field when displaying to humans
37
37
  # @param verbose: Whether the column will be shown in the verbose output
38
+ # @param interactive: Whether the field will be show in the interactive output
38
39
  # @param header_color: Override the default color for the header
39
40
  # @param row_color: Override the default color for the row
41
+ # @param modes: Additional modes flags for the callable
40
42
  # @yieldparam model The subject the column is describing, some sort of data model
41
- def register_callable(header:, verbose: nil, header_color: nil, row_color: nil, &b)
42
- super(modes: { verbose: verbose }, header: header,
43
- header_color: header_color, row_color: row_color, &b)
43
+ def register_callable(modes: {}, header:, verbose: nil, interactive: nil, header_color: nil, row_color: nil, &b)
44
+ modes = modes.map { |m| [m, true] }.to_h if modes.is_a? Array
45
+ super(modes: modes.merge(verbose: verbose, interactive: interactive),
46
+ header: header,
47
+ header_color: header_color,
48
+ row_color: row_color,
49
+ &b)
44
50
  end
45
51
  alias_method :register_column, :register_callable
46
52
 
@@ -65,10 +71,14 @@ module OutputMode
65
71
  #
66
72
  # An interative/ non-interactive output can be forced by setting the
67
73
  # +interactive+ flag to +true+/+false+ respectively
68
- def build_output(verbose: nil, ascii: false, interactive: nil, header_color: [:blue, :bold], row_color: :green)
74
+ def build_output(verbose: nil, ascii: nil, interactive: nil, header_color: [:blue, :bold], row_color: :green, context: {})
69
75
  # Set the interactive and verbose flags if not provided
70
76
  interactive = $stdout.tty? if interactive.nil?
71
77
  verbose = !interactive if verbose.nil?
78
+ ascii = !interactive if ascii.nil?
79
+
80
+ # Update the rendering context with the verbosity/interactive settings
81
+ context = context.merge(interactive: interactive, verbose: verbose, ascii: ascii)
72
82
 
73
83
  callables = if verbose
74
84
  # Filter out columns that are explicitly not verbose
@@ -78,6 +88,14 @@ module OutputMode
78
88
  output_callables.reject(&:verbose?)
79
89
  end
80
90
 
91
+ callables = if interactive
92
+ # Filter out columns that are explicitly not interactive
93
+ callables.select { |o| o.interactive?(true) }
94
+ else
95
+ # Filter out columns that are explicitly interactive
96
+ callables.reject { |o| o.interactive? }
97
+ end
98
+
81
99
  if interactive
82
100
  # Creates the human readable output
83
101
  opts = if ascii
@@ -85,20 +103,22 @@ module OutputMode
85
103
  else
86
104
  {
87
105
  yes: '✓', no: '✕', renderer: :unicode, colorize: TTY::Color.color?,
88
- header_color: callables.map { |c| c.config[:header_color] || header_color },
89
- row_color: callables.map { |c| c.config[:row_color] || row_color }
106
+ header_color: header_color,
107
+ row_color: row_color
90
108
  }
91
109
  end
92
110
 
93
111
  Outputs::Tabulated.new(*callables,
94
- header: callables.map { |c| c.config.fetch(:header, 'missing') },
112
+ rotate: false,
95
113
  padding: [0,1],
96
114
  default: '(none)',
115
+ context: context,
97
116
  **opts
98
117
  )
99
118
  else
100
119
  # Creates the machine readable output
101
- Outputs::Delimited.new(*callables, col_sep: "\t", yes: 'yes', no: 'no', default: '')
120
+ Outputs::Delimited.new(*callables, col_sep: "\t", yes: 'yes', no: 'no', default: nil,
121
+ context: context)
102
122
  end
103
123
  end
104
124
  end
@@ -35,10 +35,16 @@ module OutputMode
35
35
  # @overload register_callable(header:, verbose: true)
36
36
  # @param header: The human readable key to the field, uses the term 'header' for consistency
37
37
  # @param verbose: Whether the field will be shown in the verbose output
38
+ # @param interactive: Whether the field will be show in the interactive output
38
39
  # @param section: Define the grouping a callable belongs to. Ignored by default
40
+ # @param modes: Additional modes flags for the callable
39
41
  # @yieldparam model The subject the column is describing, some sort of data model
40
- def register_callable(header:, verbose: nil, section: :other, &b)
41
- super(modes: { verbose: verbose }, header: header, section: section, &b)
42
+ def register_callable(modes: {}, header:, verbose: nil, interactive: nil, section: :other, &b)
43
+ modes = modes.map { |m| [m, true] }.to_h if modes.is_a? Array
44
+ super(modes: modes.merge(verbose: verbose, interactive: interactive),
45
+ header: header,
46
+ section: section,
47
+ &b)
42
48
  end
43
49
  alias_method :register_attribute, :register_callable
44
50
 
@@ -65,10 +71,14 @@ module OutputMode
65
71
  #
66
72
  # An interative/ non-interactive output can be forced by setting the
67
73
  # +interactive+ flag to +true+/+false+ respectively
68
- def build_output(verbose: nil, ascii: false, interactive: nil, template: nil)
74
+ def build_output(verbose: nil, ascii: nil, interactive: nil, template: nil, context: {})
69
75
  # Set the interactive and verbose flags if not provided
70
76
  interactive = $stdout.tty? if interactive.nil?
71
77
  verbose = !interactive if verbose.nil?
78
+ ascii = !interactive if ascii.nil?
79
+
80
+ # Update the rendering context with the verbosity/interactive settings
81
+ context = context.merge(interactive: interactive, verbose: verbose, ascii: ascii)
72
82
 
73
83
  callables = if verbose
74
84
  # Filter out columns that are explicitly not verbose
@@ -78,6 +88,14 @@ module OutputMode
78
88
  output_callables.reject(&:verbose?)
79
89
  end
80
90
 
91
+ callables = if interactive
92
+ # Filter out columns that are explicitly not interactive
93
+ callables.select { |o| o.interactive?(true) }
94
+ else
95
+ # Filter out columns that are explicitly interactive
96
+ callables.reject { |o| o.interactive? }
97
+ end
98
+
81
99
  if interactive
82
100
  # Creates the human readable output
83
101
  opts = if ascii
@@ -93,10 +111,12 @@ module OutputMode
93
111
  default: '(none)',
94
112
  sections: sections,
95
113
  template: template,
114
+ context: context,
96
115
  **opts)
97
116
  else
98
117
  # Creates the machine readable output
99
- Outputs::Delimited.new(*callables, col_sep: "\t", yes: 'yes', no: 'no', default: '')
118
+ Outputs::Delimited.new(*callables, col_sep: "\t", yes: 'yes', no: 'no', default: nil,
119
+ context: context)
100
120
  end
101
121
  end
102
122
  end
@@ -25,5 +25,5 @@
25
25
  #==============================================================================
26
26
 
27
27
  module OutputMode
28
- VERSION = "1.4.0"
28
+ VERSION = "1.5.0"
29
29
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: output_mode
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - William McCumsite
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-11-05 00:00:00.000000000 Z
11
+ date: 2021-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tty-table