output_mode 1.3.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aba3d3f1c1ef5bd16dc8397b9500218ccc652af7dc6c11d85dfbb16cd7ec0ee0
4
- data.tar.gz: 9568bee91c9a89576fa24a97e8d0fba2cbf86f28fb4d2f6ede3093b773bade17
3
+ metadata.gz: e4d5f1fa1744eeff5535f33a65593b370d317124736089a8b24e2d76cb824b6e
4
+ data.tar.gz: 10e77ade81138ebe9c6ca3e6b3299ba864c213603e54682eebb611057f72e2d6
5
5
  SHA512:
6
- metadata.gz: 409dc9ad72f0a1771ad325995905bde702dcae4d3839b983c7cf02a5ace8927ef9cecc70954666dbce3a634b144d0ee8e8bf3f7981a55edcc52c480545ce36eb
7
- data.tar.gz: 8663a174778b268e3ea596a81856ff2f53b3a4d3387e68e329bddb38f6d3e002bfc8838d3634304a7680cf8a998db99468dc18b210356259522b1c69aea34d00
6
+ metadata.gz: a754089932ba840c19e0070b03e99e10fd4a957e4204ee08806dde91d54d51899950f817b8a34d3736f6c1fa9df4f85321fd938909c381f12230fe53df312ed1
7
+ data.tar.gz: 7d5858053e4d55868893bbfa2c13ed63a52bd39f05f6b4ae690a29072c0ab7f7a4d9919204c6fd387dc2bd0ad19efec2fa5e0d023e22929452d2efa616cdf6d6
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/bundle ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'bundle' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "rubygems"
12
+
13
+ m = Module.new do
14
+ module_function
15
+
16
+ def invoked_as_script?
17
+ File.expand_path($0) == File.expand_path(__FILE__)
18
+ end
19
+
20
+ def env_var_version
21
+ ENV["BUNDLER_VERSION"]
22
+ end
23
+
24
+ def cli_arg_version
25
+ return unless invoked_as_script? # don't want to hijack other binstubs
26
+ return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
27
+ bundler_version = nil
28
+ update_index = nil
29
+ ARGV.each_with_index do |a, i|
30
+ if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
31
+ bundler_version = a
32
+ end
33
+ next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
34
+ bundler_version = $1
35
+ update_index = i
36
+ end
37
+ bundler_version
38
+ end
39
+
40
+ def gemfile
41
+ gemfile = ENV["BUNDLE_GEMFILE"]
42
+ return gemfile if gemfile && !gemfile.empty?
43
+
44
+ File.expand_path("../../Gemfile", __FILE__)
45
+ end
46
+
47
+ def lockfile
48
+ lockfile =
49
+ case File.basename(gemfile)
50
+ when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
51
+ else "#{gemfile}.lock"
52
+ end
53
+ File.expand_path(lockfile)
54
+ end
55
+
56
+ def lockfile_version
57
+ return unless File.file?(lockfile)
58
+ lockfile_contents = File.read(lockfile)
59
+ return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
60
+ Regexp.last_match(1)
61
+ end
62
+
63
+ def bundler_version
64
+ @bundler_version ||=
65
+ env_var_version || cli_arg_version ||
66
+ lockfile_version
67
+ end
68
+
69
+ def bundler_requirement
70
+ return "#{Gem::Requirement.default}.a" unless bundler_version
71
+
72
+ bundler_gem_version = Gem::Version.new(bundler_version)
73
+
74
+ requirement = bundler_gem_version.approximate_recommendation
75
+
76
+ return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0")
77
+
78
+ requirement += ".a" if bundler_gem_version.prerelease?
79
+
80
+ requirement
81
+ end
82
+
83
+ def load_bundler!
84
+ ENV["BUNDLE_GEMFILE"] ||= gemfile
85
+
86
+ activate_bundler
87
+ end
88
+
89
+ def activate_bundler
90
+ gem_error = activation_error_handling do
91
+ gem "bundler", bundler_requirement
92
+ end
93
+ return if gem_error.nil?
94
+ require_error = activation_error_handling do
95
+ require "bundler/version"
96
+ end
97
+ return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
98
+ warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
99
+ exit 42
100
+ end
101
+
102
+ def activation_error_handling
103
+ yield
104
+ nil
105
+ rescue StandardError, LoadError => e
106
+ e
107
+ end
108
+ end
109
+
110
+ m.load_bundler!
111
+
112
+ if m.invoked_as_script?
113
+ load Gem.bin_path("bundler", "bundle")
114
+ end
data/bin/coderay ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'coderay' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("coderay", "coderay")
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]
@@ -74,6 +91,12 @@ puts <<~EOF
74
91
  #==============================================================================
75
92
  #==============================================================================
76
93
 
94
+ #==============================================================================
95
+ # Default Demo Index
96
+ # Simplified in interactive shells, verbose in non-interactive
97
+ #==============================================================================
98
+ #{DemoIndex.build_output.render(*data)}
99
+
77
100
  #==============================================================================
78
101
  # Demo Verbose Index
79
102
  #==============================================================================
@@ -81,7 +104,6 @@ puts <<~EOF
81
104
 
82
105
  #==============================================================================
83
106
  # Demo "Simplified" Index
84
- # NOTE: Disabled for non-interactive shell, shows the verbose output instead
85
107
  #==============================================================================
86
108
  #{DemoIndex.build_output(verbose: false).render(*data)}
87
109
 
@@ -108,6 +130,12 @@ puts <<~EOF
108
130
  #==============================================================================
109
131
  #==============================================================================
110
132
 
133
+ #==============================================================================
134
+ # Default Settings
135
+ # Simplified in interactive shells, verbose in non-interactive
136
+ #==============================================================================
137
+ #{DemoShow.build_output.render(*data)}
138
+
111
139
  #==============================================================================
112
140
  # Demo Verbose Show
113
141
  #==============================================================================
@@ -115,7 +143,6 @@ puts <<~EOF
115
143
 
116
144
  #==============================================================================
117
145
  # Demo "Simplified" Show
118
- # NOTE: Disabled for non-interactive shell, shows the verbose output instead
119
146
  #==============================================================================
120
147
  #{DemoShow.build_output(verbose: false).render(*data)}
121
148
 
data/bin/htmldiff ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'htmldiff' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("diff-lcs", "htmldiff")
data/bin/ldiff ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'ldiff' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("diff-lcs", "ldiff")
data/bin/pry ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'pry' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("pry", "pry")
data/bin/rake ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rake", "rake")
data/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
@@ -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,19 @@
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
+ when Array, Callables
37
+ callables.each do |c|
38
+ @callables << (c.is_a?(Callable) ? c : Callable.new(&c))
36
39
  end
37
40
  when nil
38
- super()
41
+ # NOOP
39
42
  else
40
43
  raise "Can not convert #{callables.class} into a #{self.class}"
41
44
  end
@@ -43,13 +46,65 @@ module OutputMode
43
46
 
44
47
  def <<(item)
45
48
  if item.is_a? Callable
46
- super
49
+ @callables << item
47
50
  elsif item.respond_to?(:call)
48
- super(Callable.new(&item))
51
+ @callables << Callable.new(&item)
49
52
  else
50
53
  raise Error, "#{item.class} is not callable"
51
54
  end
52
55
  end
56
+
57
+ def each(&block)
58
+ @callables.each(&block)
59
+ end
60
+
61
+ def pad_each(*ctx, **input_opts)
62
+ fields = self.map do |callables|
63
+ field = callables.config[:header]
64
+ if field.respond_to?(:call)
65
+ opts = if field.parameters.include?(:keyrest)
66
+ input_opts.dup
67
+ else
68
+ keys = field.parameters
69
+ .select { |type, _| [:key, :keyreq].include?(type) }
70
+ .map { |_, k| k }
71
+ input_opts.slice(*keys)
72
+ end
73
+ opts.empty? ? field.call(*ctx) : field.call(*ctx, **opts)
74
+ else
75
+ field
76
+ end
77
+ end
78
+
79
+ max_length = fields.map { |f| f.to_s.length }.max
80
+ pads = self.each_with_index.map do |callable, idx|
81
+ field = fields[idx]
82
+ length = max_length - field.to_s.length
83
+ [callable, { padding: ' ' * length, field: field }]
84
+ end
85
+
86
+ if block_given?
87
+ pads.each { |c, opts| yield(c, **opts) }
88
+ else
89
+ pads.each
90
+ end
91
+ end
92
+
93
+ def config_select(key, *values)
94
+ selected = self.select do |callable|
95
+ conf = callable.config[key]
96
+ if conf.is_a? Array
97
+ !(conf & values).empty?
98
+ else
99
+ values.include?(conf)
100
+ end
101
+ end
102
+ Callables.new(selected)
103
+ end
104
+
105
+ def length
106
+ @callables.length
107
+ end
53
108
  end
54
109
 
55
110
  class Callable
@@ -141,6 +196,29 @@ module OutputMode
141
196
  def call(*a)
142
197
  callable.call(*a)
143
198
  end
199
+
200
+ def generator(output)
201
+ ->(*a) do
202
+ # Implicitly determine which parts of the context can be passed through
203
+ ctx = if callable.parameters.any? { |type, _| type == :keyrest }
204
+ output.context
205
+ else
206
+ keys = callable.parameters.select { |type, _| [:key, :keyreq].include?(type) }
207
+ .map { |_, k| k }
208
+ output.context.slice(*keys)
209
+ end
210
+ raw = call(*a, **ctx)
211
+ if raw == true
212
+ config[:yes] || output.yes
213
+ elsif raw == false
214
+ config[:no] || output.no
215
+ elsif [nil, ''].include?(raw)
216
+ config[:default] || output.default
217
+ else
218
+ raw
219
+ end
220
+ end
221
+ end
144
222
  end
145
223
  end
146
224
 
@@ -1,5 +1,27 @@
1
1
  #==============================================================================
2
- # Refer to LICENSE.txt for licensing terms
2
+ # Copyright 2021 William McCumstie
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # 1. Redistributions of source code must retain the above copyright notice,
8
+ # this list of conditions and the following disclaimer.
9
+ #
10
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
18
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24
+ # POSSIBILITY OF SUCH DAMAGE.
3
25
  #==============================================================================
4
26
 
5
27
  require 'erb'
@@ -0,0 +1,41 @@
1
+ #==============================================================================
2
+ # Copyright 2021 William McCumstie
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are met:
6
+ #
7
+ # 1. Redistributions of source code must retain the above copyright notice,
8
+ # this list of conditions and the following disclaimer.
9
+ #
10
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ #
14
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
18
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
19
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
20
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
21
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
22
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
23
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
24
+ # POSSIBILITY OF SUCH DAMAGE.
25
+ #==============================================================================
26
+
27
+ require 'erb'
28
+
29
+ module OutputMode
30
+ NON_INTERACTIVE_ERB = ERB.new(<<~TEMPLATE, nil, '-')
31
+ <% each do |value, field:, padding:, **_| -%>
32
+ <% if value.nil? && field.nil? -%>
33
+ \t
34
+ <% elsif field.nil? -%>
35
+ \t<%= value %>
36
+ <% else -%>
37
+ <%= field -%>\t<%= value.to_s.dump[1...-1] %>
38
+ <% end -%>
39
+ <% end -%>
40
+ TEMPLATE
41
+ end
@@ -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,31 +71,38 @@ 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
 
83
78
  private
84
79
 
80
+ def has_header?
81
+ callables.any? { |c| c.config.key?(:header) }
82
+ end
83
+
85
84
  # Colorizes the header when requested
86
85
  def processed_header
87
- header&.each_with_index&.map do |h, idx|
88
- color = index_selector(:header_color, idx)
86
+ return nil unless has_header?
87
+ callables.map do |callable|
88
+ header = callable.config.fetch(:header, '')
89
+ color = callable.config.fetch(:header_color, nil) || header_color
89
90
  case color
90
91
  when nil
91
- h.to_s
92
+ header.to_s
92
93
  when Array
93
- pastel.decorate(h.to_s, *color)
94
+ pastel.decorate(header.to_s, *color)
94
95
  else
95
- pastel.decorate(h.to_s, color)
96
+ pastel.decorate(header.to_s, color)
96
97
  end
97
98
  end
98
99
  end
99
100
 
100
101
  # 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)
102
+ def process_row(model)
103
+ callables.map do |callable|
104
+ d = callable.generator(self).call(model)
105
+ color = callable.config[:row_color] || row_color
104
106
  case color
105
107
  when NilClass
106
108
  d.to_s
@@ -38,27 +38,24 @@ 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(model, **output.context).map do |callable, padding:, field:|
53
+ value = callable.generator(output).call(model)
54
+ [value, { field: field, padding: padding }]
61
55
  end
56
+
57
+ # Runs the provided block
58
+ objs.each(&block)
62
59
  end
63
60
 
64
61
  # 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,8 +71,16 @@ 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: false, ascii: false, interactive: nil, header_color: [:blue, :bold], row_color: :green)
69
- callables = if verbose || !$stdout.tty?
74
+ def build_output(verbose: nil, ascii: nil, interactive: nil, header_color: [:blue, :bold], row_color: :green, context: {})
75
+ # Set the interactive and verbose flags if not provided
76
+ interactive = $stdout.tty? if interactive.nil?
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)
82
+
83
+ callables = if verbose
70
84
  # Filter out columns that are explicitly not verbose
71
85
  output_callables.select { |o| o.verbose?(true) }
72
86
  else
@@ -74,27 +88,37 @@ module OutputMode
74
88
  output_callables.reject(&:verbose?)
75
89
  end
76
90
 
77
- if interactive || (interactive.nil? && $stdout.tty?)
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
+
99
+ if interactive
78
100
  # Creates the human readable output
79
101
  opts = if ascii
80
102
  { yes: 'yes', no: 'no', renderer: :ascii }
81
103
  else
82
104
  {
83
105
  yes: '✓', no: '✕', renderer: :unicode, colorize: TTY::Color.color?,
84
- header_color: callables.map { |c| c.config[:header_color] || header_color },
85
- row_color: callables.map { |c| c.config[:row_color] || row_color }
106
+ header_color: header_color,
107
+ row_color: row_color
86
108
  }
87
109
  end
88
110
 
89
111
  Outputs::Tabulated.new(*callables,
90
- header: callables.map { |c| c.config.fetch(:header, 'missing') },
112
+ rotate: false,
91
113
  padding: [0,1],
92
114
  default: '(none)',
115
+ context: context,
93
116
  **opts
94
117
  )
95
118
  else
96
119
  # Creates the machine readable output
97
- 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)
98
122
  end
99
123
  end
100
124
  end
@@ -25,6 +25,8 @@
25
25
  #==============================================================================
26
26
 
27
27
  require 'tty-color'
28
+ require 'output_mode/default_erb'
29
+ require 'output_mode/non_interactive_erb'
28
30
 
29
31
  module OutputMode
30
32
  module TLDR
@@ -35,10 +37,16 @@ module OutputMode
35
37
  # @overload register_callable(header:, verbose: true)
36
38
  # @param header: The human readable key to the field, uses the term 'header' for consistency
37
39
  # @param verbose: Whether the field will be shown in the verbose output
40
+ # @param interactive: Whether the field will be show in the interactive output
38
41
  # @param section: Define the grouping a callable belongs to. Ignored by default
42
+ # @param modes: Additional modes flags for the callable
39
43
  # @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)
44
+ def register_callable(modes: {}, header:, verbose: nil, interactive: nil, section: :default, &b)
45
+ modes = modes.map { |m| [m, true] }.to_h if modes.is_a? Array
46
+ super(modes: modes.merge(verbose: verbose, interactive: interactive),
47
+ header: header,
48
+ section: section,
49
+ &b)
42
50
  end
43
51
  alias_method :register_attribute, :register_callable
44
52
 
@@ -61,12 +69,24 @@ module OutputMode
61
69
  # for consumption by machines. This output ignores the provided +verbose+
62
70
  # flag as it is always verbose.
63
71
  #
64
- # The +template+ overrides the default erb template for the output
72
+ # The +template+ overrides the default erb template for interactive sessions.
73
+ # The +non_interactive_template+ overrides the template for non-interactive
74
+ # sessions.
65
75
  #
66
76
  # An interative/ non-interactive output can be forced by setting the
67
77
  # +interactive+ flag to +true+/+false+ respectively
68
- def build_output(verbose: false, ascii: false, interactive: nil, template: nil)
69
- callables = if verbose || !$stdout.tty?
78
+ def build_output(verbose: nil, ascii: nil, interactive: nil, context: {},
79
+ template: OutputMode::DEFAULT_ERB,
80
+ non_interactive_template: OutputMode::NON_INTERACTIVE_ERB)
81
+ # Set the interactive and verbose flags if not provided
82
+ interactive = $stdout.tty? if interactive.nil?
83
+ verbose = !interactive if verbose.nil?
84
+ ascii = !interactive if ascii.nil?
85
+
86
+ # Update the rendering context with the verbosity/interactive settings
87
+ context = context.merge(interactive: interactive, verbose: verbose, ascii: ascii)
88
+
89
+ callables = if verbose
70
90
  # Filter out columns that are explicitly not verbose
71
91
  output_callables.select { |o| o.verbose?(true) }
72
92
  else
@@ -74,26 +94,30 @@ module OutputMode
74
94
  output_callables.reject(&:verbose?)
75
95
  end
76
96
 
77
- if interactive || (interactive.nil? && $stdout.tty?)
78
- # Creates the human readable output
79
- opts = if ascii
80
- { yes: 'yes', no: 'no', colorize: false }
81
- else
82
- { yes: '✓', no: '✕', colorize: TTY::Color.color? }
83
- end
84
-
85
- sections = callables.map { |o| o.config[:section] }
86
-
87
- Outputs::Templated.new(*callables,
88
- fields: callables.map { |c| c.config.fetch(:header, 'missing') },
89
- default: '(none)',
90
- sections: sections,
91
- template: template,
92
- **opts)
97
+ callables = if interactive
98
+ # Filter out columns that are explicitly not interactive
99
+ callables.select { |o| o.interactive?(true) }
93
100
  else
94
- # Creates the machine readable output
95
- Outputs::Delimited.new(*callables, col_sep: "\t", yes: 'yes', no: 'no', default: '')
101
+ # Filter out columns that are explicitly interactive
102
+ callables.reject { |o| o.interactive? }
96
103
  end
104
+
105
+ # Define the templating parameters
106
+ opts = if ascii && interactive
107
+ { yes: 'yes', no: 'no', colorize: false, default: '(none)', template: template }
108
+ elsif interactive
109
+ { yes: '✓', no: '✕', colorize: TTY::Color.color?, default: '(none)', template: template }
110
+ else
111
+ { yes: 'yes', no: 'no', colorize: false, default: '', template: non_interactive_template }
112
+ end
113
+
114
+ sections = callables.map { |o| o.config[:section] }
115
+
116
+ Outputs::Templated.new(*callables,
117
+ fields: callables.map { |c| c.config.fetch(:header, 'missing') },
118
+ sections: sections,
119
+ context: context,
120
+ **opts)
97
121
  end
98
122
  end
99
123
  end
@@ -25,5 +25,5 @@
25
25
  #==============================================================================
26
26
 
27
27
  module OutputMode
28
- VERSION = "1.3.0"
28
+ VERSION = "1.6.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.3.0
4
+ version: 1.6.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-04 00:00:00.000000000 Z
11
+ date: 2021-05-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tty-table
@@ -122,14 +122,22 @@ files:
122
122
  - LICENSE.txt
123
123
  - README.md
124
124
  - Rakefile
125
+ - bin/bundle
126
+ - bin/coderay
125
127
  - bin/console
126
128
  - bin/demo
129
+ - bin/htmldiff
130
+ - bin/ldiff
131
+ - bin/pry
132
+ - bin/rake
133
+ - bin/rspec
127
134
  - bin/setup
128
135
  - lib/output_mode.rb
129
136
  - lib/output_mode/builder_dsl.rb
130
137
  - lib/output_mode/callable.rb
131
138
  - lib/output_mode/default_erb.rb
132
139
  - lib/output_mode/errors.rb
140
+ - lib/output_mode/non_interactive_erb.rb
133
141
  - lib/output_mode/output.rb
134
142
  - lib/output_mode/outputs.rb
135
143
  - lib/output_mode/outputs/delimited.rb