Chrononaut-hirb 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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