Chrononaut-hirb 0.2.1

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.
@@ -0,0 +1,17 @@
1
+ require 'ostruct'
2
+
3
+ class Hirb::HashStruct < OpenStruct #:nodoc:
4
+ def self.block_to_hash(block=nil)
5
+ config = self.new
6
+ if block
7
+ block.call(config)
8
+ config.to_hash
9
+ else
10
+ {}
11
+ end
12
+ end
13
+
14
+ def to_hash
15
+ @table
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ module Hirb
2
+ module Helpers #:nodoc:
3
+ end
4
+ end
5
+ %w{table object_table active_record_table auto_table tree parent_child_tree}.each do |e|
6
+ require "hirb/helpers/#{e}"
7
+ end
@@ -0,0 +1,16 @@
1
+ class Hirb::Helpers::ActiveRecordTable < Hirb::Helpers::ObjectTable
2
+ # Rows are Rails' ActiveRecord::Base objects.
3
+ # Takes same options as Hirb::Helpers::Table.render except as noted below.
4
+ #
5
+ # Options:
6
+ # :fields- Can be any attribute, column or not. If not given, this defaults to the database table's columns.
7
+ def self.render(rows, options={})
8
+ rows = [rows] unless rows.is_a?(Array)
9
+ options[:fields] ||=
10
+ begin
11
+ fields = rows.first.class.column_names
12
+ fields.map {|e| e.to_sym }
13
+ end
14
+ super(rows, options)
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # Detects the table class the output should use and delegates rendering to it.
2
+ class Hirb::Helpers::AutoTable
3
+ # Same options as Hirb::Helpers::Table.render.
4
+ def self.render(output, options={})
5
+ output = output.to_a if !output.is_a?(Array) && output.respond_to?(:to_a)
6
+ klass = if ((output.is_a?(Array) && output[0].is_a?(ActiveRecord::Base)) or output.is_a?(ActiveRecord::Base) rescue false)
7
+ Hirb::Helpers::ActiveRecordTable
8
+ elsif (output.is_a?(Array) && !(output[0].is_a?(Hash) || output[0].is_a?(Array)))
9
+ Hirb::Helpers::ObjectTable
10
+ else
11
+ Hirb::Helpers::Table
12
+ end
13
+ klass.render(output, options)
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ class Hirb::Helpers::ObjectTable < Hirb::Helpers::Table
2
+ # Rows are any ruby objects. Takes same options as Hirb::Helpers::Table.render except as noted below.
3
+ #
4
+ # Options:
5
+ # :fields- Methods of the object to represent as columns. Defaults to [:to_s].
6
+ def self.render(rows, options ={})
7
+ options[:fields] ||= [:to_s]
8
+ options[:headers] = {:to_s=>'value'} if options[:fields] == [:to_s]
9
+ rows = [rows] unless rows.is_a?(Array)
10
+ item_hashes = rows.inject([]) {|t,item|
11
+ t << options[:fields].inject({}) {|h,f| h[f] = item.send(f); h}
12
+ }
13
+ super(item_hashes, options)
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ class Hirb::Helpers::ParentChildTree < Hirb::Helpers::Tree
2
+ class <<self
3
+ # Starting with the given node, this builds a tree by recursively calling a children method.
4
+ # Takes same options as Hirb::Helper::Table.render with some additional ones below.
5
+ # ==== Options:
6
+ # [:value_method] Method to call to display as a node's value. If not given, uses :name if node
7
+ # responds to :name or defaults to :object_id.
8
+ # [:children_method] Method to call to obtain a node's children. Default is :children.
9
+ def render(root_node, options={})
10
+ @value_method = options[:value_method] || (root_node.respond_to?(:name) ? :name : :object_id)
11
+ @children_method = options[:children_method] || :children
12
+ @nodes = []
13
+ build_node(root_node, 0)
14
+ super(@nodes, options)
15
+ end
16
+
17
+ def build_node(node, level) #:nodoc:
18
+ @nodes << {:value=>node.send(@value_method), :level=>level}
19
+ node.send(@children_method).each {|e| build_node(e, level + 1)}
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,243 @@
1
+ # Base Table class from which other table classes inherit.
2
+ # By default, a table is constrained to a default width but this can be adjusted
3
+ # via the max_width option or Hirb::View.width.
4
+ # Rows can be an array of arrays or an array of hashes.
5
+ #
6
+ # An array of arrays ie [[1,2], [2,3]], would render:
7
+ # +---+---+
8
+ # | 0 | 1 |
9
+ # +---+---+
10
+ # | 1 | 2 |
11
+ # | 2 | 3 |
12
+ # +---+---+
13
+ #
14
+ # By default, the fields/columns are the numerical indices of the array.
15
+ #
16
+ # An array of hashes ie [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], would render:
17
+ # +-----+--------+
18
+ # | age | weight |
19
+ # +-----+--------+
20
+ # | 10 | 100 |
21
+ # | 80 | 500 |
22
+ # +-----+--------+
23
+ #
24
+ # By default, the fields/columns are the keys of the first hash.
25
+ #--
26
+ # derived from http://gist.github.com/72234
27
+ class Hirb::Helpers::Table
28
+ BORDER_LENGTH = 3 # " | " and "-+-" are the borders
29
+ class TooManyFieldsForWidthError < StandardError; end
30
+
31
+ class << self
32
+
33
+ # Main method which returns a formatted table.
34
+ # ==== Options:
35
+ # [:fields] An array which overrides the default fields and can be used to indicate field order.
36
+ # [:headers] A hash of fields and their header names. Fields that aren't specified here default to their name.
37
+ # This option can also be an array but only for array rows.
38
+ # [:field_lengths] A hash of fields and their maximum allowed lengths. If a field exceeds it's maximum
39
+ # length than it's truncated and has a ... appended to it. Fields that aren't specified here have no maximum allowed
40
+ # length.
41
+ # [:max_width] The maximum allowed width of all fields put together. This option is enforced except when the field_lengths option is set.
42
+ # This doesn't count field borders as part of the total.
43
+ # [:number] When set to true, numbers rows by adding a :hirb_number column as the first column. Default is false.
44
+ # [:filters] A hash of fields and the filters that each row in the field must run through. The filter converts the cell's value by applying
45
+ # a given proc or an array containing a method and optional arguments to it.
46
+ # Examples:
47
+ # Hirb::Helpers::Table.render [[1,2], [2,3]]
48
+ # Hirb::Helpers::Table.render [[1,2], [2,3]], :field_lengths=>{0=>10}
49
+ # Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}]
50
+ # Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], :headers=>{:weight=>"Weight(lbs)"}
51
+ # Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], :filters=>{:age=>[:to_f]}
52
+ def render(rows, options={})
53
+ new(rows,options).render
54
+ end
55
+ end
56
+
57
+ #:stopdoc:
58
+ def initialize(rows, options={})
59
+ @options = options
60
+ @options[:filters] ||= {}
61
+ @vertical = options[:vertical] || (Hirb::View.config[:vertical] if Hirb::View.config)
62
+ @fields = @options[:fields] ? @options[:fields].dup : ((rows[0].is_a?(Hash)) ? rows[0].keys.sort {|a,b| a.to_s <=> b.to_s} :
63
+ rows[0].is_a?(Array) ? (0..rows[0].length - 1).to_a : [])
64
+ if @vertical
65
+ @longest = @fields.map.sort_by { |i| i.to_s.length }.last.to_s.length
66
+ end
67
+ @rows = setup_rows(rows)
68
+ @headers = @fields.inject({}) {|h,e| h[e] = e.to_s; h}
69
+ if @options.has_key?(:headers)
70
+ @headers = @options[:headers].is_a?(Hash) ? @headers.merge(@options[:headers]) :
71
+ (@options[:headers].is_a?(Array) ? array_to_indices_hash(@options[:headers]) : @options[:headers])
72
+ end
73
+ if @options[:number]
74
+ @headers[:hirb_number] = "number"
75
+ @fields.unshift :hirb_number
76
+ end
77
+ end
78
+
79
+ def setup_rows(rows)
80
+ rows ||= []
81
+ rows = [rows] unless rows.is_a?(Array)
82
+ if rows[0].is_a?(Array)
83
+ rows = rows.inject([]) {|new_rows, row|
84
+ new_rows << array_to_indices_hash(row)
85
+ }
86
+ end
87
+ rows = filter_values(rows)
88
+ rows.each_with_index {|e,i| e[:hirb_number] = (i + 1).to_s} if @options[:number] or @vertical
89
+ validate_values(rows)
90
+ rows
91
+ end
92
+
93
+ def render
94
+ body = []
95
+ unless @rows.length == 0
96
+ setup_field_lengths
97
+ body += @headers ? render_header : [render_border] unless @vertical
98
+ body += render_rows
99
+ body << render_border unless @vertical
100
+ end
101
+ body << render_table_description
102
+ body.join("\n")
103
+ end
104
+
105
+ def render_header
106
+ title_row = '| ' + @fields.map {|f|
107
+ format_cell(@headers[f], @field_lengths[f])
108
+ }.join(' | ') + ' |'
109
+ [render_border, title_row, render_border]
110
+ end
111
+
112
+ def render_border
113
+ '+-' + @fields.map {|f| '-' * @field_lengths[f] }.join('-+-') + '-+'
114
+ end
115
+
116
+ def format_cell(value, cell_width)
117
+ text = value.length > cell_width ?
118
+ (
119
+ (cell_width < 5) ? value.slice(0,cell_width) : value.slice(0, cell_width - 3) + '...'
120
+ ) : value
121
+ sprintf("%-#{cell_width}s", text)
122
+ end
123
+
124
+ def render_rows
125
+ i = 0
126
+ @rows.map do |row|
127
+ if @vertical
128
+ stars = "*" * [(@longest + (@longest / 2)), 3].max
129
+ row = "#{stars} #{@rows[i][:hirb_number]}. row #{stars}\n" +
130
+ @fields.map {|f|
131
+ "#{f.to_s.rjust(@longest)}: #{row[f]}"
132
+ }.join("\n")
133
+ else
134
+ row = '| ' + @fields.map {|f|
135
+ format_cell(row[f], @field_lengths[f])
136
+ }.join(' | ') + ' |'
137
+ end
138
+ i += 1
139
+ row
140
+ end
141
+ end
142
+
143
+ def render_table_description
144
+ (@rows.length == 0) ? "0 rows in set" :
145
+ "#{@rows.length} #{@rows.length == 1 ? 'row' : 'rows'} in set"
146
+ end
147
+
148
+ def setup_field_lengths
149
+ @field_lengths = default_field_lengths
150
+ if @options[:field_lengths]
151
+ @field_lengths.merge!(@options[:field_lengths])
152
+ else
153
+ table_max_width = @options.has_key?(:max_width) ? @options[:max_width] : Hirb::View.width
154
+ restrict_field_lengths(@field_lengths, table_max_width) if table_max_width
155
+ end
156
+ end
157
+
158
+ def restrict_field_lengths(field_lengths, max_width)
159
+ max_width -= @fields.size * BORDER_LENGTH + 1
160
+ original_field_lengths = field_lengths.dup
161
+ @min_field_length = BORDER_LENGTH
162
+ adjust_long_fields(field_lengths, max_width)
163
+ rescue TooManyFieldsForWidthError
164
+ raise
165
+ rescue
166
+ default_restrict_field_lengths(field_lengths, original_field_lengths, max_width)
167
+ end
168
+
169
+ # Simple algorithm which given a max width, allows smaller fields to be displayed while
170
+ # restricting longer fields at an average_long_field_length.
171
+ def adjust_long_fields(field_lengths, max_width)
172
+ total_length = field_lengths.values.inject {|t,n| t += n}
173
+ while total_length > max_width
174
+ raise TooManyFieldsForWidthError if @fields.size > max_width.to_f / @min_field_length
175
+ average_field_length = total_length / @fields.size.to_f
176
+ long_lengths = field_lengths.values.select {|e| e > average_field_length}
177
+ if long_lengths.empty?
178
+ raise "Algorithm didn't work, resort to default"
179
+ else
180
+ total_long_field_length = (long_lengths.inject {|t,n| t += n}) * max_width/total_length
181
+ average_long_field_length = total_long_field_length / long_lengths.size
182
+ field_lengths.each {|f,length|
183
+ field_lengths[f] = average_long_field_length if length > average_long_field_length
184
+ }
185
+ end
186
+ total_length = field_lengths.values.inject {|t,n| t += n}
187
+ end
188
+ end
189
+
190
+ # Produces a field_lengths which meets the max_width requirement
191
+ def default_restrict_field_lengths(field_lengths, original_field_lengths, max_width)
192
+ original_total_length = original_field_lengths.values.inject {|t,n| t += n}
193
+ relative_lengths = original_field_lengths.values.map {|v| (v / original_total_length.to_f * max_width).to_i }
194
+ # set fields by their relative weight to original length
195
+ if relative_lengths.all? {|e| e > @min_field_length} && (relative_lengths.inject {|a,e| a += e} <= max_width)
196
+ original_field_lengths.each {|k,v| field_lengths[k] = (v / original_total_length.to_f * max_width).to_i }
197
+ else
198
+ # set all fields the same if nothing else works
199
+ field_lengths.each {|k,v| field_lengths[k] = max_width / @fields.size}
200
+ end
201
+ end
202
+
203
+ # find max length for each field; start with the headers
204
+ def default_field_lengths
205
+ field_lengths = @headers ? @headers.inject({}) {|h,(k,v)| h[k] = v.length; h} : {}
206
+ @rows.each do |row|
207
+ @fields.each do |field|
208
+ len = row[field].length
209
+ field_lengths[field] = len if len > field_lengths[field].to_i
210
+ end
211
+ end
212
+ field_lengths
213
+ end
214
+
215
+ def filter_values(rows)
216
+ rows.map {|row|
217
+ new_row = {}
218
+ @fields.each {|f|
219
+ if @options[:filters][f]
220
+ new_row[f] = @options[:filters][f].is_a?(Proc) ? @options[:filters][f].call(row[f]) :
221
+ row[f].send(*@options[:filters][f])
222
+ else
223
+ new_row[f] = row[f]
224
+ end
225
+ }
226
+ new_row
227
+ }
228
+ end
229
+
230
+ def validate_values(rows)
231
+ rows.each {|row|
232
+ @fields.each {|f|
233
+ row[f] = row[f].to_s || ''
234
+ }
235
+ }
236
+ end
237
+
238
+ # Converts an array to a hash mapping a numerical index to its array value.
239
+ def array_to_indices_hash(array)
240
+ array.inject({}) {|hash,e| hash[hash.size] = e; hash }
241
+ end
242
+ #:startdoc:
243
+ end
@@ -0,0 +1,177 @@
1
+ # Base tree class which given an array of nodes produces different types of trees.
2
+ # The types of trees currently are:
3
+ # * basic:
4
+ # 0
5
+ # 1
6
+ # 2
7
+ # 3
8
+ # 4
9
+ #
10
+ # * directory:
11
+ # 0
12
+ # |-- 1
13
+ # | |-- 2
14
+ # | `-- 3
15
+ # `-- 4
16
+ #
17
+ # * number:
18
+ # 1. 0
19
+ # 1. 1
20
+ # 1. 2
21
+ # 2. 3
22
+ # 2. 4
23
+ #
24
+ # Tree nodes can be given as an array of arrays or an array of hashes.
25
+ # To render the above basic tree with an array of hashes:
26
+ # Hirb::Helpers::Tree.render([{:value=>0, :level=>0}, {:value=>1, :level=>1}, {:value=>2, :level=>2},
27
+ # {:value=>3, :level=>2}, {:value=>4, :level=>1}])
28
+ # Note from the hash keys that :level refers to the depth of the tree while :value refers to the text displayed
29
+ # for a node.
30
+ #
31
+ # To render the above basic tree with an array of arrays:
32
+ # Hirb::Helpers::Tree.render([[0,0], [1,1], [2,2], [2,3], [1,4]])
33
+ # Note that the each array pair consists of the level and the value for the node.
34
+ class Hirb::Helpers::Tree
35
+ class ParentlessNodeError < StandardError; end
36
+
37
+ class <<self
38
+ # Main method which renders a tree.
39
+ # ==== Options:
40
+ # [:type] Type of tree. Either :basic, :directory or :number. Default is :basic.
41
+ # [:validate] Boolean to validate tree. Checks to see if all nodes have parents. Raises ParentlessNodeError if
42
+ # an invalid node is found. Default is false.
43
+ # [:indent] Number of spaces to indent between levels for basic + number trees. Default is 4.
44
+ # [:limit] Limits the level or depth of a tree that is displayed. Root node is level 0.
45
+ # [:description] Displays brief description about tree ie how many nodes it has.
46
+ # Examples:
47
+ # Hirb::Helpers::Tree.render([[0, 'root'], [1, 'child']], :type=>:directory)
48
+ def render(nodes, options={})
49
+ new(nodes, options).render
50
+ end
51
+ end
52
+
53
+ # :stopdoc:
54
+ attr_accessor :nodes
55
+
56
+ def initialize(input_nodes, options={})
57
+ @options = options
58
+ @type = options[:type] || :basic
59
+ if input_nodes[0].is_a?(Array)
60
+ @nodes = input_nodes.map {|e| Node.new(:level=>e[0], :value=>e[1]) }
61
+ else
62
+ @nodes = input_nodes.map {|e| Node.new(e)}
63
+ end
64
+ @nodes.each_with_index {|e,i| e.merge!(:tree=>self, :index=>i)}
65
+ @nodes.each {|e| e[:value] = e[:value].to_s }
66
+ validate_nodes if options[:validate]
67
+ self
68
+ end
69
+
70
+ def render
71
+ body = render_tree
72
+ body += render_description if @options[:description]
73
+ body
74
+ end
75
+
76
+ def render_description
77
+ "\n\n#{@nodes.length} #{@nodes.length == 1 ? 'node' : 'nodes'} in tree"
78
+ end
79
+
80
+ def render_tree
81
+ @indent = ' ' * (@options[:indent] || 4 )
82
+ @nodes = @nodes.select {|e| e[:level] <= @options[:limit] } if @options[:limit]
83
+ case @type.to_s
84
+ when 'directory'
85
+ render_directory
86
+ when 'number'
87
+ render_number
88
+ else
89
+ render_basic
90
+ end
91
+ end
92
+
93
+ def render_directory
94
+ mark_last_nodes_per_level
95
+ new_nodes = []
96
+ @nodes.each_with_index {|e, i|
97
+ value = ''
98
+ unless e.root?
99
+ value << e.render_parent_characters
100
+ value << (e[:last_node] ? "`-- " : "|-- ")
101
+ end
102
+ value << e[:value]
103
+ new_nodes << value
104
+ }
105
+ new_nodes.join("\n")
106
+ end
107
+
108
+ def render_number
109
+ counter = {}
110
+ @nodes.each {|e|
111
+ parent_level_key = "#{(e.parent ||{})[:index]}.#{e[:level]}"
112
+ counter[parent_level_key] ||= 0
113
+ counter[parent_level_key] += 1
114
+ e[:pre_value] = "#{counter[parent_level_key]}. "
115
+ }
116
+ @nodes.map {|e| @indent * e[:level] + e[:pre_value] + e[:value]}.join("\n")
117
+ end
118
+
119
+ def render_basic
120
+ @nodes.map {|e| @indent * e[:level] + e[:value]}.join("\n")
121
+ end
122
+
123
+ def validate_nodes
124
+ @nodes.each do |e|
125
+ raise ParentlessNodeError if (e[:level] > e.previous[:level]) && (e[:level] - e.previous[:level]) > 1
126
+ end
127
+ end
128
+
129
+ # walks tree accumulating last nodes per unique parent+level
130
+ def mark_last_nodes_per_level
131
+ @nodes.each {|e| e.delete(:last_node)}
132
+ last_node_hash = @nodes.inject({}) {|h,e|
133
+ h["#{(e.parent ||{})[:index]}.#{e[:level]}"] = e; h
134
+ }
135
+ last_node_hash.values.uniq.each {|e| e[:last_node] = true}
136
+ end
137
+ #:startdoc:
138
+ class Node < ::Hash #:nodoc:
139
+ class MissingLevelError < StandardError; end
140
+ class MissingValueError < StandardError; end
141
+
142
+ def initialize(hash)
143
+ super
144
+ raise MissingLevelError unless hash.has_key?(:level)
145
+ raise MissingValueError unless hash.has_key?(:value)
146
+ replace(hash)
147
+ end
148
+
149
+ def parent
150
+ self[:tree].nodes.slice(0 .. self[:index]).reverse.detect {|e| e[:level] < self[:level]}
151
+ end
152
+
153
+ def next
154
+ self[:tree].nodes[self[:index] + 1]
155
+ end
156
+
157
+ def previous
158
+ self[:tree].nodes[self[:index] - 1]
159
+ end
160
+
161
+ def root?; self[:level] == 0; end
162
+
163
+ # refers to characters which connect parent nodes
164
+ def render_parent_characters
165
+ parent_chars = []
166
+ get_parents_character(parent_chars)
167
+ parent_chars.reverse.map {|level| level + ' ' * 3 }.to_s
168
+ end
169
+
170
+ def get_parents_character(parent_chars)
171
+ if self.parent
172
+ parent_chars << (self.parent[:last_node] ? ' ' : '|') unless self.parent.root?
173
+ self.parent.get_parents_character(parent_chars)
174
+ end
175
+ end
176
+ end
177
+ end