hsume2-hirb 0.6.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.gemspec +21 -0
  2. data/CHANGELOG.rdoc +144 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.rdoc +194 -0
  5. data/Rakefile +35 -0
  6. data/lib/bond/completions/hirb.rb +15 -0
  7. data/lib/hirb/console.rb +43 -0
  8. data/lib/hirb/dynamic_view.rb +113 -0
  9. data/lib/hirb/formatter.rb +126 -0
  10. data/lib/hirb/helpers/auto_table.rb +24 -0
  11. data/lib/hirb/helpers/object_table.rb +14 -0
  12. data/lib/hirb/helpers/parent_child_tree.rb +24 -0
  13. data/lib/hirb/helpers/tab_table.rb +24 -0
  14. data/lib/hirb/helpers/table/filters.rb +10 -0
  15. data/lib/hirb/helpers/table/resizer.rb +82 -0
  16. data/lib/hirb/helpers/table.rb +349 -0
  17. data/lib/hirb/helpers/tree.rb +181 -0
  18. data/lib/hirb/helpers/unicode_table.rb +15 -0
  19. data/lib/hirb/helpers/vertical_table.rb +37 -0
  20. data/lib/hirb/helpers.rb +18 -0
  21. data/lib/hirb/import_object.rb +10 -0
  22. data/lib/hirb/menu.rb +238 -0
  23. data/lib/hirb/pager.rb +105 -0
  24. data/lib/hirb/string.rb +44 -0
  25. data/lib/hirb/util.rb +96 -0
  26. data/lib/hirb/version.rb +3 -0
  27. data/lib/hirb/view.rb +270 -0
  28. data/lib/hirb/views/couch_db.rb +11 -0
  29. data/lib/hirb/views/misc_db.rb +15 -0
  30. data/lib/hirb/views/mongo_db.rb +14 -0
  31. data/lib/hirb/views/orm.rb +11 -0
  32. data/lib/hirb/views/rails.rb +19 -0
  33. data/lib/hirb/views.rb +8 -0
  34. data/lib/hirb.rb +82 -0
  35. data/lib/ripl/hirb.rb +15 -0
  36. data/test/auto_table_test.rb +30 -0
  37. data/test/console_test.rb +27 -0
  38. data/test/deps.rip +4 -0
  39. data/test/dynamic_view_test.rb +94 -0
  40. data/test/formatter_test.rb +176 -0
  41. data/test/hirb_test.rb +39 -0
  42. data/test/import_test.rb +9 -0
  43. data/test/menu_test.rb +255 -0
  44. data/test/object_table_test.rb +79 -0
  45. data/test/pager_test.rb +162 -0
  46. data/test/resizer_test.rb +62 -0
  47. data/test/table_test.rb +630 -0
  48. data/test/test_helper.rb +61 -0
  49. data/test/tree_test.rb +184 -0
  50. data/test/util_test.rb +59 -0
  51. data/test/view_test.rb +165 -0
  52. data/test/views_test.rb +13 -0
  53. metadata +184 -0
@@ -0,0 +1,126 @@
1
+ module Hirb
2
+ # A Formatter object formats an output object (using Formatter.format_output) into a string based on the views defined
3
+ # for its class and/or ancestry.
4
+ class Formatter
5
+ class<<self
6
+ # This config is used by Formatter.format_output to lazily load dynamic views defined with Hirb::DynamicView.
7
+ # This hash has the same format as Formatter.config.
8
+ attr_accessor :dynamic_config
9
+
10
+ # Array of classes whose objects respond to :to_a and allow the first
11
+ # element of the converted array to determine the output class.
12
+ attr_accessor :to_a_classes
13
+ end
14
+ self.dynamic_config = {}
15
+ self.to_a_classes = %w{Array Set ActiveRecord::Relation}
16
+
17
+ def initialize(additional_config={}) #:nodoc:
18
+ @klass_config = {}
19
+ @config = additional_config || {}
20
+ end
21
+
22
+ # A hash of Ruby class strings mapped to view hashes. A view hash must have at least a :method, :output_method
23
+ # or :class option for a view to be applied to an output. A view hash has the following keys:
24
+ # [*:method*] Specifies a global (Kernel) method to do the formatting.
25
+ # [*:class*] Specifies a class to do the formatting, using its render() class method. If a symbol it's converted to a corresponding
26
+ # Hirb::Helpers::* class if it exists.
27
+ # [*:output_method*] Specifies a method or proc to call on output before passing it to a helper. If the output is an array, it's applied
28
+ # to every element in the array.
29
+ # [*:options*] Options to pass the helper method or class.
30
+ # [*:ancestor*] Boolean which when true causes subclasses of the output class to inherit its config. This doesn't effect the current
31
+ # output class. Defaults to false. This is used by ActiveRecord classes.
32
+ #
33
+ # Examples:
34
+ # {'WWW::Delicious::Element'=>{:class=>'Hirb::Helpers::ObjectTable', :ancestor=>true, :options=>{:max_width=>180}}}
35
+ # {'Date'=>{:class=>:auto_table, :ancestor=>true}}
36
+ # {'Hash'=>{:method=>:puts}}
37
+ def config
38
+ @config
39
+ end
40
+
41
+ # Adds the view for the given class and view hash config. See Formatter.config for valid keys for view hash.
42
+ def add_view(klass, view_config)
43
+ @klass_config.delete(klass)
44
+ @config[klass.to_s] = view_config
45
+ true
46
+ end
47
+
48
+ # This method looks for an output object's view in Formatter.config and then Formatter.dynamic_config.
49
+ # If a view is found, a stringified view is returned based on the object. If no view is found, nil is returned. The options this
50
+ # class takes are a view hash as described in Formatter.config. These options will be merged with any existing helper
51
+ # config hash an output class has in Formatter.config. Any block given is passed along to a helper class.
52
+ def format_output(output, options={}, &block)
53
+ output_class = determine_output_class(output)
54
+ options = parse_console_options(options) if options.delete(:console)
55
+ options = Util.recursive_hash_merge(klass_config(output_class), options)
56
+ _format_output(output, options, &block)
57
+ end
58
+
59
+ #:stopdoc:
60
+ def to_a_classes
61
+ @to_a_classes ||= self.class.to_a_classes.map {|e| Util.any_const_get(e) }.compact
62
+ end
63
+
64
+ def _format_output(output, options, &block)
65
+ output = options[:output_method] ? (output.is_a?(Array) ?
66
+ output.map {|e| call_output_method(options[:output_method], e) } :
67
+ call_output_method(options[:output_method], output) ) : output
68
+ args = [output]
69
+ args << options[:options] if options[:options] && !options[:options].empty?
70
+ if options[:method]
71
+ send(options[:method],*args)
72
+ elsif options[:class] && (helper_class = Helpers.helper_class(options[:class]))
73
+ helper_class.render(*args, &block)
74
+ elsif options[:output_method]
75
+ output
76
+ end
77
+ end
78
+
79
+ def parse_console_options(options) #:nodoc:
80
+ real_options = [:method, :class, :output_method].inject({}) do |h, e|
81
+ h[e] = options.delete(e) if options[e]; h
82
+ end
83
+ real_options.merge! :options=>options
84
+ end
85
+
86
+ def determine_output_class(output)
87
+ output.respond_to?(:to_a) && to_a_classes.any? {|e| output.is_a?(e) } ?
88
+ Array(output)[0].class : output.class
89
+ end
90
+
91
+ def call_output_method(output_method, output)
92
+ output_method.is_a?(Proc) ? output_method.call(output) : output.send(output_method)
93
+ end
94
+
95
+ # Internal view options built from user-defined ones. Options are built by recursively merging options from oldest
96
+ # ancestors to the most recent ones.
97
+ def klass_config(output_class)
98
+ @klass_config[output_class] ||= build_klass_config(output_class)
99
+ end
100
+
101
+ def build_klass_config(output_class)
102
+ output_ancestors = output_class.ancestors.map {|e| e.to_s}.reverse
103
+ output_ancestors.pop
104
+ hash = output_ancestors.inject({}) {|h, klass|
105
+ add_klass_config_if_true(h, klass) {|c,klass| c[klass] && c[klass][:ancestor] }
106
+ }
107
+ add_klass_config_if_true(hash, output_class.to_s) {|c,klass| c[klass] }
108
+ end
109
+
110
+ def add_klass_config_if_true(hash, klass)
111
+ if yield(@config, klass)
112
+ Util.recursive_hash_merge hash, @config[klass]
113
+ elsif yield(self.class.dynamic_config, klass)
114
+ @config[klass] = self.class.dynamic_config[klass].dup # copy to local
115
+ Util.recursive_hash_merge hash, self.class.dynamic_config[klass]
116
+ else
117
+ hash
118
+ end
119
+ end
120
+
121
+ def reset_klass_config
122
+ @klass_config = {}
123
+ end
124
+ #:startdoc:
125
+ end
126
+ end
@@ -0,0 +1,24 @@
1
+ # This helper wraps around the other table helpers i.e. Hirb::Helpers::Table while
2
+ # providing default helper options via Hirb::DynamicView. Using these default options, this
3
+ # helper supports views for the following modules/classes:
4
+ # ActiveRecord::Base, CouchFoo::Base, CouchPotato::Persistence, CouchRest::ExtendedDocument,
5
+ # DBI::Row, DataMapper::Resource, Friendly::Document, MongoMapper::Document, MongoMapper::EmbeddedDocument,
6
+ # Mongoid::Document, Ripple::Document, Sequel::Model.
7
+ class Hirb::Helpers::AutoTable < Hirb::Helpers::Table
8
+ extend Hirb::DynamicView
9
+
10
+ # Takes same options as Hirb::Helpers::Table.render except as noted below.
11
+ #
12
+ # ==== Options:
13
+ # [:table_class] Explicit table class to use for rendering. Defaults to
14
+ # Hirb::Helpers::ObjectTable if output is not an Array or Hash. Otherwise
15
+ # defaults to Hirb::Helpers::Table.
16
+ def self.render(output, options={})
17
+ output = Array(output)
18
+ (defaults = dynamic_options(output[0])) && (options = defaults.merge(options))
19
+ klass = options.delete(:table_class) || (
20
+ !(output[0].is_a?(Hash) || output[0].is_a?(Array)) ?
21
+ Hirb::Helpers::ObjectTable : Hirb::Helpers::Table)
22
+ klass.render(output, options)
23
+ end
24
+ end
@@ -0,0 +1,14 @@
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
+ item_hashes = options[:fields].empty? ? [] : Array(rows).inject([]) {|t,item|
10
+ t << options[:fields].inject({}) {|h,f| h[f] = item.__send__(f); h}
11
+ }
12
+ super(item_hashes, options)
13
+ end
14
+ end
@@ -0,0 +1,24 @@
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 or proc 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 or proc 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
+ @value_method = value_method.is_a?(Proc) ? value_method : lambda {|n| n.send(value_method) }
12
+ children_method = options[:children_method] || :children
13
+ @children_method = children_method.is_a?(Proc) ? children_method : lambda {|n| n.send(children_method)}
14
+ @nodes = []
15
+ build_node(root_node, 0)
16
+ super(@nodes, options)
17
+ end
18
+
19
+ def build_node(node, level) #:nodoc:
20
+ @nodes << {:value=>@value_method.call(node), :level=>level}
21
+ @children_method.call(node).each {|e| build_node(e, level + 1)}
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ class Hirb::Helpers::TabTable < Hirb::Helpers::Table
2
+ DELIM = "\t"
3
+
4
+ # Renders a tab-delimited table
5
+ def self.render(rows, options={})
6
+ new(rows, {:description => false}.merge(options)).render
7
+ end
8
+
9
+ def render_header
10
+ @headers ? render_table_header : []
11
+ end
12
+
13
+ def render_table_header
14
+ [ format_values(@headers).join(DELIM) ]
15
+ end
16
+
17
+ def render_rows
18
+ @rows.map { |row| format_values(row).join(DELIM) }
19
+ end
20
+
21
+ def render_footer
22
+ []
23
+ end
24
+ end
@@ -0,0 +1,10 @@
1
+ class Hirb::Helpers::Table
2
+ # Contains filter methods used by :filters option. To define a custom filter, simply open this module and create a method
3
+ # that take one argument, the value you will be filtering.
4
+ module Filters
5
+ extend self
6
+ def comma_join(arr) #:nodoc:
7
+ arr.join(', ')
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,82 @@
1
+ class Hirb::Helpers::Table
2
+ # Resizes a table's fields to the table's max width.
3
+ class Resizer
4
+ # Modifies field_lengths to fit within width. Also enforces a table's max_fields.
5
+ def self.resize!(table)
6
+ obj = new(table)
7
+ obj.resize
8
+ obj.field_lengths
9
+ end
10
+
11
+ #:stopdoc:
12
+ attr_reader :field_lengths
13
+ def initialize(table)
14
+ @table, @width, @field_size = table, table.actual_width, table.fields.size
15
+ @field_lengths = table.field_lengths
16
+ @original_field_lengths = @field_lengths.dup
17
+ end
18
+
19
+ def resize
20
+ adjust_long_fields || default_restrict_field_lengths
21
+ @table.enforce_field_constraints
22
+ add_extra_width
23
+ end
24
+
25
+ # Simple algorithm which allows smaller fields to be displayed while
26
+ # restricting longer fields to an average_long_field
27
+ def adjust_long_fields
28
+ while (total_length = sum(@field_lengths.values)) > @width
29
+ average_field = total_length / @field_size.to_f
30
+ long_lengths = @field_lengths.values.select {|e| e > average_field }
31
+ return false if long_lengths.empty?
32
+
33
+ # adjusts average long field by ratio with @width
34
+ average_long_field = sum(long_lengths)/long_lengths.size * @width/total_length
35
+ @field_lengths.each {|f,length|
36
+ @field_lengths[f] = average_long_field if length > average_long_field
37
+ }
38
+ end
39
+ true
40
+ end
41
+
42
+ # Produces a field_lengths which meets the @width requirement
43
+ def default_restrict_field_lengths
44
+ original_total_length = sum @original_field_lengths.values
45
+ # set fields by their relative weight to original length
46
+ new_lengths = @original_field_lengths.inject({}) {|t,(k,v)|
47
+ t[k] = (v / original_total_length.to_f * @width).to_i; t }
48
+
49
+ # set all fields the same if relative doesn't work
50
+ unless new_lengths.values.all? {|e| e > MIN_FIELD_LENGTH} && (sum(new_lengths.values) <= @width)
51
+ new_lengths = @field_lengths.inject({}) {|t,(k,v)| t[k] = @width / @field_size; t }
52
+ end
53
+ @field_lengths.each {|k,v| @field_lengths[k] = new_lengths[k] }
54
+ end
55
+
56
+ def add_extra_width
57
+ added_width = 0
58
+ extra_width = @width - sum(@field_lengths.values)
59
+ unmaxed_fields = @field_lengths.keys.select {|f| !remaining_width(f).zero? }
60
+ # order can affect which one gets the remainder so let's keep it consistent
61
+ unmaxed_fields = unmaxed_fields.sort_by {|e| e.to_s}
62
+
63
+ unmaxed_fields.each_with_index do |f, i|
64
+ extra_per_field = (extra_width - added_width) / (unmaxed_fields.size - i)
65
+ add_to_field = remaining_width(f) < extra_per_field ? remaining_width(f) : extra_per_field
66
+ added_width += add_to_field
67
+ @field_lengths[f] += add_to_field
68
+ end
69
+ end
70
+
71
+ def remaining_width(field)
72
+ (@remaining_width ||= {})[field] ||= begin
73
+ (@table.max_fields[field] || @original_field_lengths[field]) - @field_lengths[field]
74
+ end
75
+ end
76
+
77
+ def sum(arr)
78
+ arr.inject {|t,e| t += e } || 0
79
+ end
80
+ #:startdoc:
81
+ end
82
+ end
@@ -0,0 +1,349 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require 'hirb/helpers/table/filters'
3
+ require 'hirb/helpers/table/resizer'
4
+
5
+ module Hirb
6
+ # Base Table class from which other table classes inherit.
7
+ # By default, a table is constrained to a default width but this can be adjusted
8
+ # via the max_width option or Hirb::View.width.
9
+ # Rows can be an array of arrays or an array of hashes.
10
+ #
11
+ # An array of arrays ie [[1,2], [2,3]], would render:
12
+ # +---+---+
13
+ # | 0 | 1 |
14
+ # +---+---+
15
+ # | 1 | 2 |
16
+ # | 2 | 3 |
17
+ # +---+---+
18
+ #
19
+ # By default, the fields/columns are the numerical indices of the array.
20
+ #
21
+ # An array of hashes ie [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], would render:
22
+ # +-----+--------+
23
+ # | age | weight |
24
+ # +-----+--------+
25
+ # | 10 | 100 |
26
+ # | 80 | 500 |
27
+ # +-----+--------+
28
+ #
29
+ # By default, the fields/columns are the keys of the first hash.
30
+ #
31
+ # === Custom Callbacks
32
+ # Callback methods can be defined to add your own options that modify rows right before they are rendered.
33
+ # Here's an example that allows for searching with a :query option:
34
+ # module Query
35
+ # # Searches fields given a query hash
36
+ # def query_callback(rows, options)
37
+ # return rows unless options[:query]
38
+ # options[:query].map {|field,query|
39
+ # rows.select {|e| e[field].to_s =~ /#{query}/i }
40
+ # }.flatten.uniq
41
+ # end
42
+ # end
43
+ # Hirb::Helpers::Table.send :include, Query
44
+ #
45
+ # >> puts Hirb::Helpers::Table.render [{:name=>'batman'}, {:name=>'robin'}], :query=>{:name=>'rob'}
46
+ # +-------+
47
+ # | name |
48
+ # +-------+
49
+ # | robin |
50
+ # +-------+
51
+ # 1 row in set
52
+ #
53
+ # Callback methods:
54
+ # * must be defined in Helpers::Table and end in '_callback'.
55
+ # * should expect rows and a hash of render options. Rows will be an array of hashes.
56
+ # * are expected to return an array of hashes.
57
+ # * are invoked in alphabetical order.
58
+ # For a thorough example, see {Boson::Pipe}[http://github.com/cldwalker/boson/blob/master/lib/boson/pipe.rb].
59
+ #--
60
+ # derived from http://gist.github.com/72234
61
+ class Helpers::Table
62
+ BORDER_LENGTH = 3 # " | " and "-+-" are the borders
63
+ MIN_FIELD_LENGTH = 3
64
+ class TooManyFieldsForWidthError < StandardError; end
65
+
66
+ CHARS = {
67
+ :top => {:left => '+', :center => '+', :right => '+', :horizontal => '-',
68
+ :vertical => {:outside => '|', :inside => '|'} },
69
+ :middle => {:left => '+', :center => '+', :right => '+', :horizontal => '-'},
70
+ :bottom => {:left => '+', :center => '+', :right => '+', :horizontal => '-',
71
+ :vertical => {:outside => '|', :inside => '|'} }
72
+ }
73
+
74
+ class << self
75
+
76
+ # Main method which returns a formatted table.
77
+ # ==== Options:
78
+ # [*:fields*] An array which overrides the default fields and can be used to indicate field order.
79
+ # [*:headers*] A hash of fields and their header names. Fields that aren't specified here default to their name.
80
+ # When set to false, headers are hidden. Can also be an array but only for array rows.
81
+ # [*:max_fields*] A hash of fields and their maximum allowed lengths. Maximum length can also be a percentage of the total width
82
+ # (decimal less than one). When a field exceeds it's maximum then it's
83
+ # truncated and has a ... appended to it. Fields that aren't specified have no maximum.
84
+ # [*:max_width*] The maximum allowed width of all fields put together including field borders. Only valid when :resize is true.
85
+ # Default is Hirb::View.width.
86
+ # [*:resize*] Resizes table to display all columns in allowed :max_width. Default is true. Setting this false will display the full
87
+ # length of each field.
88
+ # [*:number*] When set to true, numbers rows by adding a :hirb_number column as the first column. Default is false.
89
+ # [*:change_fields*] A hash to change old field names to new field names. This can also be an array of new names but only for array rows.
90
+ # This is useful when wanting to change auto-generated keys to more user-friendly names i.e. for array rows.
91
+ # [*:grep_fields*] A regexp that selects which fields to display. By default this is not set and applied.
92
+ # [*:filters*] A hash of fields and their filters, applied to every row in a field. A filter can be a proc, an instance method
93
+ # applied to the field value or a Filters method. Also see the filter_classes attribute below.
94
+ # [*:header_filter*] A filter, like one in :filters, that is applied to all headers after the :headers option.
95
+ # [*:filter_any*] When set to true, any cell defaults to being filtered by its class in :filter_classes.
96
+ # Default Hirb::Helpers::Table.filter_any().
97
+ # [*:filter_classes*] Hash which maps classes to filters. Default is Hirb::Helpers::Table.filter_classes().
98
+ # [*:vertical*] When set to true, renders a vertical table using Hirb::Helpers::VerticalTable. Default is false.
99
+ # [*:unicode*] When set to true, renders a unicode table using Hirb::Helpers::UnicodeTable. Default is false.
100
+ # [*:tab*] When set to true, renders a tab-delimited table using Hirb::Helpers::TabTable. Default is false.
101
+ # [*:all_fields*] When set to true, renders fields in all rows. Valid only in rows that are hashes. Default is false.
102
+ # [*:description*] When set to true, renders row count description at bottom. Default is true.
103
+ # [*:escape_special_chars*] When set to true, escapes special characters \n,\t,\r so they don't disrupt tables. Default is false for
104
+ # vertical tables and true for anything else.
105
+ # Examples:
106
+ # Hirb::Helpers::Table.render [[1,2], [2,3]]
107
+ # Hirb::Helpers::Table.render [[1,2], [2,3]], :max_fields=>{0=>10}, :header_filter=>:capitalize
108
+ # Hirb::Helpers::Table.render [['a',1], ['b',2]], :change_fields=>%w{letters numbers}, :max_fields=>{'numbers'=>0.4}
109
+ # Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}]
110
+ # Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], :headers=>{:weight=>"Weight(lbs)"}
111
+ # Hirb::Helpers::Table.render [{:age=>10, :weight=>100}, {:age=>80, :weight=>500}], :filters=>{:age=>[:to_f]}
112
+ def render(rows, options={})
113
+ options[:vertical] ? Helpers::VerticalTable.render(rows, options) :
114
+ options[:unicode] ? Helpers::UnicodeTable.render(rows, options) :
115
+ options[:tab] ? Helpers::TabTable.render(rows, options) :
116
+ new(rows, options).render
117
+ rescue TooManyFieldsForWidthError
118
+ $stderr.puts "", "** Hirb Warning: Too many fields for the current width. Configure your width " +
119
+ "and/or fields to avoid this error. Defaulting to a vertical table. **"
120
+ Helpers::VerticalTable.render(rows, options)
121
+ end
122
+
123
+ # A hash which maps a cell value's class to a filter. This serves to set a default filter per field if all of its
124
+ # values are a class in this hash. By default, Array values are comma joined and Hashes are inspected.
125
+ # See the :filter_any option to apply this filter per value.
126
+ attr_accessor :filter_classes
127
+ # Boolean which sets the default for :filter_any option.
128
+ attr_accessor :filter_any
129
+ # Holds last table object created
130
+ attr_accessor :last_table
131
+ end
132
+ self.filter_classes = { Array=>:comma_join, Hash=>:inspect }
133
+
134
+ def chars
135
+ self.class.const_get(:CHARS)
136
+ end
137
+
138
+ #:stopdoc:
139
+ attr_accessor :width, :max_fields, :field_lengths, :fields
140
+ def initialize(rows, options={})
141
+ raise ArgumentError, "Table must be an array of hashes or array of arrays" unless rows.is_a?(Array) &&
142
+ (rows[0].is_a?(Hash) or rows[0].is_a?(Array) or rows.empty?)
143
+ @options = {:description=>true, :filters=>{}, :change_fields=>{}, :escape_special_chars=>true,
144
+ :filter_any=>Helpers::Table.filter_any, :resize=>true}.merge(options)
145
+ @fields = set_fields(rows)
146
+ @fields = @fields.select {|e| e.to_s[@options[:grep_fields]] } if @options[:grep_fields]
147
+ @rows = set_rows(rows)
148
+ @headers = set_headers
149
+ if @options[:number]
150
+ @headers[:hirb_number] ||= "number"
151
+ @fields.unshift :hirb_number
152
+ end
153
+ Helpers::Table.last_table = self
154
+ end
155
+
156
+ def set_fields(rows)
157
+ @options[:change_fields] = array_to_indices_hash(@options[:change_fields]) if @options[:change_fields].is_a?(Array)
158
+ return @options[:fields].dup if @options[:fields]
159
+
160
+ fields = if rows[0].is_a?(Hash)
161
+ keys = @options[:all_fields] ? rows.map {|e| e.keys}.flatten.uniq : rows[0].keys
162
+ keys.sort {|a,b| a.to_s <=> b.to_s}
163
+ else
164
+ rows[0].is_a?(Array) ? (0..rows[0].length - 1).to_a : []
165
+ end
166
+
167
+ @options[:change_fields].each do |oldf, newf|
168
+ (index = fields.index(oldf)) && fields[index] = newf
169
+ end
170
+ fields
171
+ end
172
+
173
+ def set_rows(rows)
174
+ rows = Array(rows)
175
+ if rows[0].is_a?(Array)
176
+ rows = rows.inject([]) {|new_rows, row|
177
+ new_rows << array_to_indices_hash(row)
178
+ }
179
+ end
180
+ @options[:change_fields].each do |oldf, newf|
181
+ rows.each {|e| e[newf] = e.delete(oldf) if e.key?(oldf) }
182
+ end
183
+ rows = filter_values(rows)
184
+ rows.each_with_index {|e,i| e[:hirb_number] = (i + 1).to_s} if @options[:number]
185
+ deleted_callbacks = Array(@options[:delete_callbacks]).map {|e| "#{e}_callback" }
186
+ (methods.grep(/_callback$/).map {|e| e.to_s} - deleted_callbacks).sort.each do |meth|
187
+ rows = send(meth, rows, @options.dup)
188
+ end
189
+ validate_values(rows)
190
+ rows
191
+ end
192
+
193
+ def set_headers
194
+ headers = @fields.inject({}) {|h,e| h[e] = e.to_s; h}
195
+ if @options.has_key?(:headers)
196
+ headers = @options[:headers].is_a?(Hash) ? headers.merge(@options[:headers]) :
197
+ (@options[:headers].is_a?(Array) ? array_to_indices_hash(@options[:headers]) : @options[:headers])
198
+ end
199
+ if @options[:header_filter]
200
+ headers.each {|k,v|
201
+ headers[k] = call_filter(@options[:header_filter], v)
202
+ }
203
+ end
204
+ headers
205
+ end
206
+
207
+ def render
208
+ body = []
209
+ unless @rows.length == 0
210
+ setup_field_lengths
211
+ body += render_header
212
+ body += render_rows
213
+ body += render_footer
214
+ end
215
+ body << render_table_description if @options[:description]
216
+ body.join("\n")
217
+ end
218
+
219
+ def render_header
220
+ @headers ? render_table_header : [render_border(:top)]
221
+ end
222
+
223
+ def render_footer
224
+ [render_border(:bottom)]
225
+ end
226
+
227
+ def render_table_header
228
+ title_row = chars[:top][:vertical][:outside] + ' ' +
229
+ format_values(@headers).join(' ' + chars[:top][:vertical][:inside] +' ') +
230
+ ' ' + chars[:top][:vertical][:outside]
231
+ [render_border(:top), title_row, render_border(:middle)]
232
+ end
233
+
234
+ def render_border(which)
235
+ chars[which][:left] + chars[which][:horizontal] +
236
+ @fields.map {|f| chars[which][:horizontal] * @field_lengths[f] }.
237
+ join(chars[which][:horizontal] + chars[which][:center] + chars[which][:horizontal]) +
238
+ chars[which][:horizontal] + chars[which][:right]
239
+ end
240
+
241
+ def format_values(values)
242
+ @fields.map {|field| format_cell(values[field], @field_lengths[field]) }
243
+ end
244
+
245
+ def format_cell(value, cell_width)
246
+ text = String.size(value) > cell_width ?
247
+ (
248
+ (cell_width < 5) ? String.slice(value, 0, cell_width) : String.slice(value, 0, cell_width - 3) + '...'
249
+ ) : value
250
+ String.ljust(text, cell_width)
251
+ end
252
+
253
+ def render_rows
254
+ @rows.map do |row|
255
+ chars[:bottom][:vertical][:outside] + ' ' +
256
+ format_values(row).join(' ' + chars[:bottom][:vertical][:inside] + ' ') +
257
+ ' ' + chars[:bottom][:vertical][:outside]
258
+ end
259
+ end
260
+
261
+ def render_table_description
262
+ (@rows.length == 0) ? "0 rows in set" :
263
+ "#{@rows.length} #{@rows.length == 1 ? 'row' : 'rows'} in set"
264
+ end
265
+
266
+ def setup_field_lengths
267
+ @field_lengths = default_field_lengths
268
+ if @options[:resize]
269
+ raise TooManyFieldsForWidthError if @fields.size > self.actual_width.to_f / MIN_FIELD_LENGTH
270
+ Resizer.resize!(self)
271
+ else
272
+ enforce_field_constraints
273
+ end
274
+ end
275
+
276
+ def enforce_field_constraints
277
+ max_fields.each {|k,max| @field_lengths[k] = max if @field_lengths[k].to_i > max }
278
+ end
279
+
280
+ def max_fields
281
+ @max_fields ||= (@options[:max_fields] ||= {}).each {|k,v|
282
+ @options[:max_fields][k] = (actual_width * v.to_f.abs).floor if v.to_f.abs < 1
283
+ }
284
+ end
285
+
286
+ def actual_width
287
+ @actual_width ||= self.width - (@fields.size * BORDER_LENGTH + 1)
288
+ end
289
+
290
+ def width
291
+ @width ||= @options[:max_width] || View.width
292
+ end
293
+
294
+ # find max length for each field; start with the headers
295
+ def default_field_lengths
296
+ field_lengths = @headers ? @headers.inject({}) {|h,(k,v)| h[k] = String.size(v); h} :
297
+ @fields.inject({}) {|h,e| h[e] = 1; h }
298
+ @rows.each do |row|
299
+ @fields.each do |field|
300
+ len = String.size(row[field])
301
+ field_lengths[field] = len if len > field_lengths[field].to_i
302
+ end
303
+ end
304
+ field_lengths
305
+ end
306
+
307
+ def set_filter_defaults(rows)
308
+ @filter_classes.each do |klass, filter|
309
+ @fields.each {|field|
310
+ if rows.all? {|r| r[field].class == klass }
311
+ @options[:filters][field] ||= filter
312
+ end
313
+ }
314
+ end
315
+ end
316
+
317
+ def filter_values(rows)
318
+ @filter_classes = Helpers::Table.filter_classes.merge @options[:filter_classes] || {}
319
+ set_filter_defaults(rows) unless @options[:filter_any]
320
+ rows.map {|row|
321
+ @fields.inject({}) {|new_row,f|
322
+ (filter = @options[:filters][f]) || (@options[:filter_any] && (filter = @filter_classes[row[f].class]))
323
+ new_row[f] = filter ? call_filter(filter, row[f]) : row[f]
324
+ new_row
325
+ }
326
+ }
327
+ end
328
+
329
+ def call_filter(filter, val)
330
+ filter.is_a?(Proc) ? filter.call(val) :
331
+ val.respond_to?(Array(filter)[0]) ? val.send(*filter) : Filters.send(filter, val)
332
+ end
333
+
334
+ def validate_values(rows)
335
+ rows.each {|row|
336
+ @fields.each {|f|
337
+ row[f] = row[f].to_s || ''
338
+ row[f] = row[f].gsub(/(\t|\r|\n)/) {|e| e.dump.gsub('"','') } if @options[:escape_special_chars]
339
+ }
340
+ }
341
+ end
342
+
343
+ # Converts an array to a hash mapping a numerical index to its array value.
344
+ def array_to_indices_hash(array)
345
+ array.inject({}) {|hash,e| hash[hash.size] = e; hash }
346
+ end
347
+ #:startdoc:
348
+ end
349
+ end