table_print 0.2.3 → 1.0.0.rc3
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +1 -0
- data/.rvmrc +1 -1
- data/.travis.yml +5 -0
- data/Gemfile +11 -10
- data/README.rdoc +85 -32
- data/Rakefile +13 -13
- data/VERSION +1 -1
- data/features/adding_columns.feature +48 -0
- data/features/configuring_output.feature +57 -0
- data/features/excluding_columns.feature +28 -0
- data/features/sensible_defaults.feature +86 -0
- data/features/support/step_definitions/before.rb +3 -0
- data/features/support/step_definitions/steps.rb +77 -0
- data/lib/cattr.rb +46 -0
- data/lib/column.rb +45 -0
- data/lib/config.rb +36 -0
- data/lib/config_resolver.rb +91 -0
- data/lib/fingerprinter.rb +85 -0
- data/lib/formatter.rb +45 -0
- data/lib/hash_extensions.rb +37 -0
- data/lib/kernel_extensions.rb +12 -0
- data/lib/printable.rb +22 -0
- data/lib/returnable.rb +21 -0
- data/lib/row_group.rb +227 -0
- data/lib/table_print.rb +33 -389
- data/spec/column_spec.rb +71 -0
- data/spec/config_resolver_spec.rb +236 -0
- data/spec/config_spec.rb +52 -0
- data/spec/fingerprinter_spec.rb +151 -0
- data/spec/formatter_spec.rb +78 -0
- data/spec/hash_extensions_spec.rb +21 -0
- data/spec/printable_spec.rb +51 -0
- data/spec/returnable_spec.rb +23 -0
- data/spec/row_group_spec.rb +466 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/table_print_spec.rb +59 -0
- data/table_print.gemspec +50 -26
- metadata +147 -68
- data/Gemfile.lock +0 -20
- data/test/helper.rb +0 -56
- data/test/test_column.rb +0 -379
- data/test/test_table_print.rb +0 -162
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'cat'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'table_print'
|
4
|
+
|
5
|
+
Given /^a class named (.*)$/ do |klass|
|
6
|
+
Sandbox.add_class(klass)
|
7
|
+
end
|
8
|
+
|
9
|
+
Given /^(.*) has attributes (.*)$/ do |klass, attributes|
|
10
|
+
attrs = attributes.split(",").map { |attr| attr.strip }
|
11
|
+
|
12
|
+
Sandbox.add_attributes(klass, *attrs)
|
13
|
+
end
|
14
|
+
|
15
|
+
Given /^(.*) has a class method named (.*) with (.*)$/ do |klass, method_name, blk|
|
16
|
+
Sandbox.add_class_method(klass, method_name, &eval(blk))
|
17
|
+
end
|
18
|
+
|
19
|
+
Given /^(.*) has a method named (\w*) with (.*)$/ do |klass, method_name, blk|
|
20
|
+
Sandbox.add_method(klass, method_name, &eval(blk))
|
21
|
+
end
|
22
|
+
|
23
|
+
When /^I instantiate a (.*) with (\{.*\})$/ do |klass, args|
|
24
|
+
@objs ||= OpenStruct.new
|
25
|
+
@objs.send("#{klass.downcase}=", Sandbox.const_get_from_string(klass).new(eval(args)))
|
26
|
+
end
|
27
|
+
|
28
|
+
When /^I instantiate a (.*) with (\{.*\}) and (add it|assign it) to (.*)$/ do |klass, args, assignment_method, target|
|
29
|
+
# the thing we're instantiating
|
30
|
+
child = Sandbox.const_get_from_string(klass).new(eval(args))
|
31
|
+
|
32
|
+
# the place we're going to add it
|
33
|
+
method_chain = target.split(".")
|
34
|
+
target_method = method_chain.pop
|
35
|
+
target_object = method_chain.inject(@objs) { |obj, method_name| obj.send(method_name) }
|
36
|
+
|
37
|
+
# how we're going to add it
|
38
|
+
if assignment_method == "assign it"
|
39
|
+
target_object.send("#{target_method}=", child)
|
40
|
+
else
|
41
|
+
target_object.send("#{target_method}") << child
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
When /^configure (.*) with (.*)$/ do |klass, config|
|
46
|
+
klass = Sandbox.const_get_from_string(klass)
|
47
|
+
TablePrint::Config.set(klass, eval(config))
|
48
|
+
end
|
49
|
+
|
50
|
+
When /table_print ([\w:]*), (.*)$/ do |klass, options|
|
51
|
+
tp(@objs.send(klass.downcase), eval(options))
|
52
|
+
end
|
53
|
+
|
54
|
+
When /table_print ([\w\.:]*)$/ do |klass|
|
55
|
+
obj = @objs.send(klass.split(".").first.downcase)
|
56
|
+
obj = obj.send(klass.split(".").last) if klass.include? "." # hack - we're assuming only two levels. use inject to find the target.
|
57
|
+
|
58
|
+
tp(obj)
|
59
|
+
end
|
60
|
+
|
61
|
+
Then /^the output should contain$/ do |string|
|
62
|
+
output = []
|
63
|
+
while line = @r.gets
|
64
|
+
output << line
|
65
|
+
end
|
66
|
+
@r.close
|
67
|
+
|
68
|
+
output.zip(string.split("\n")).each do |actual, expected|
|
69
|
+
actual.gsub(/\s/m, "").split(//).sort.join.should == expected.gsub(" ", "").split(//).sort.join
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def tp(data, options={})
|
74
|
+
@r, w = IO.pipe
|
75
|
+
w.puts TablePrint::Printer.table_print(data, options)
|
76
|
+
w.close
|
77
|
+
end
|
data/lib/cattr.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
class Class
|
2
|
+
def cattr_reader(*syms)
|
3
|
+
syms.flatten.each do |sym|
|
4
|
+
next if sym.is_a?(Hash)
|
5
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
6
|
+
unless defined? @@#{sym}
|
7
|
+
@@#{sym} = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.#{sym}
|
11
|
+
@@#{sym}
|
12
|
+
end
|
13
|
+
|
14
|
+
def #{sym}
|
15
|
+
@@#{sym}
|
16
|
+
end
|
17
|
+
EOS
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def cattr_writer(*syms)
|
22
|
+
options = syms.last.is_a?(Hash) ? syms.pop : {}
|
23
|
+
syms.flatten.each do |sym|
|
24
|
+
class_eval(<<-EOS, __FILE__, __LINE__)
|
25
|
+
unless defined? @@#{sym}
|
26
|
+
@@#{sym} = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.#{sym}=(obj)
|
30
|
+
@@#{sym} = obj
|
31
|
+
end
|
32
|
+
|
33
|
+
#{"
|
34
|
+
def #{sym}=(obj)
|
35
|
+
@@#{sym} = obj
|
36
|
+
end
|
37
|
+
" unless options[:instance_writer] == false }
|
38
|
+
EOS
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def cattr_accessor(*syms)
|
43
|
+
cattr_reader(*syms)
|
44
|
+
cattr_writer(*syms)
|
45
|
+
end
|
46
|
+
end
|
data/lib/column.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module TablePrint
|
2
|
+
class Column
|
3
|
+
attr_reader :formatters
|
4
|
+
attr_writer :width
|
5
|
+
attr_accessor :name, :data, :time_format
|
6
|
+
|
7
|
+
def initialize(attr_hash={})
|
8
|
+
@formatters = []
|
9
|
+
attr_hash.each do |k, v|
|
10
|
+
self.send("#{k}=", v)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def name=(n)
|
15
|
+
@name = n.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def formatters=(formatter_list)
|
19
|
+
formatter_list.each do |f|
|
20
|
+
add_formatter(f)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def display_method=(method)
|
25
|
+
method = method.to_s unless method.is_a? Proc
|
26
|
+
@display_method = method
|
27
|
+
end
|
28
|
+
|
29
|
+
def display_method
|
30
|
+
@display_method ||= name
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_formatter(formatter)
|
34
|
+
@formatters << formatter
|
35
|
+
end
|
36
|
+
|
37
|
+
def data_width
|
38
|
+
[name.length].concat(data.compact.collect(&:to_s).collect(&:length)).max
|
39
|
+
end
|
40
|
+
|
41
|
+
def width
|
42
|
+
@width ||= data_width
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/config.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'cattr'
|
2
|
+
|
3
|
+
module TablePrint
|
4
|
+
class Config
|
5
|
+
cattr_accessor :max_width, :time_format
|
6
|
+
|
7
|
+
DEFAULT_MAX_WIDTH = 30
|
8
|
+
DEFAULT_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
9
|
+
|
10
|
+
@@max_width = DEFAULT_MAX_WIDTH
|
11
|
+
@@time_format = DEFAULT_TIME_FORMAT
|
12
|
+
|
13
|
+
@@klasses = {}
|
14
|
+
|
15
|
+
def self.set(klass, val)
|
16
|
+
if klass.is_a? Class
|
17
|
+
@@klasses[klass] = val # val is a hash of column options
|
18
|
+
else
|
19
|
+
TablePrint::Config.send("#{klass}=", val.first)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.for(klass)
|
24
|
+
@@klasses.fetch(klass) {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.clear(klass)
|
28
|
+
if klass.is_a? Class
|
29
|
+
@@klasses.delete(klass)
|
30
|
+
else
|
31
|
+
original_value = TablePrint::Config.const_get("DEFAULT_#{klass.to_s.upcase}")
|
32
|
+
TablePrint::Config.send("#{klass}=", original_value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'column'
|
2
|
+
require 'config'
|
3
|
+
|
4
|
+
module TablePrint
|
5
|
+
class ConfigResolver
|
6
|
+
def initialize(klass, default_column_names, *options)
|
7
|
+
@column_hash = {}
|
8
|
+
|
9
|
+
@default_columns = default_column_names.collect { |name| option_to_column(name) }
|
10
|
+
|
11
|
+
@included_columns = []
|
12
|
+
@excepted_columns = []
|
13
|
+
@only_columns = []
|
14
|
+
|
15
|
+
process_option_set(TablePrint::Config.for(klass))
|
16
|
+
process_option_set(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def process_option_set(options)
|
20
|
+
|
21
|
+
options = [options].flatten
|
22
|
+
options.delete_if { |o| o == {} }
|
23
|
+
|
24
|
+
# process special symbols
|
25
|
+
|
26
|
+
@included_columns.concat [get_and_remove(options, :include)].flatten
|
27
|
+
@included_columns.map! do |option|
|
28
|
+
if option.is_a? Column
|
29
|
+
option if option.is_a? Column
|
30
|
+
else
|
31
|
+
option_to_column(option)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
@included_columns.each do |c|
|
36
|
+
@column_hash[c.name] = c
|
37
|
+
end
|
38
|
+
|
39
|
+
# excepted columns don't need column objects since we're just going to throw them out anyway
|
40
|
+
@excepted_columns.concat [get_and_remove(options, :except)].flatten
|
41
|
+
|
42
|
+
# anything that isn't recognized as a special option is assumed to be a column name
|
43
|
+
options.compact!
|
44
|
+
@only_columns = options.collect { |name| option_to_column(name) } unless options.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_and_remove(options_array, key)
|
48
|
+
except = options_array.select do |option|
|
49
|
+
option.is_a? Hash and option.keys.include? key
|
50
|
+
end
|
51
|
+
|
52
|
+
return [] if except.empty?
|
53
|
+
except = except.first
|
54
|
+
|
55
|
+
option_of_interest = except.fetch(key)
|
56
|
+
except.delete(key)
|
57
|
+
|
58
|
+
options_array.delete(except) if except.keys.empty? # if we've taken all the info from this option, get rid of it
|
59
|
+
|
60
|
+
option_of_interest
|
61
|
+
end
|
62
|
+
|
63
|
+
def option_to_column(option)
|
64
|
+
if option.is_a? Hash
|
65
|
+
name = option.keys.first
|
66
|
+
if option[name].is_a? Proc
|
67
|
+
option = {:name => name, :display_method => option[name]}
|
68
|
+
else
|
69
|
+
option = option[name].merge(:name => name)
|
70
|
+
end
|
71
|
+
else
|
72
|
+
option = {:name => option}
|
73
|
+
end
|
74
|
+
c = Column.new(option)
|
75
|
+
@column_hash[c.name] = c
|
76
|
+
c
|
77
|
+
end
|
78
|
+
|
79
|
+
def usable_column_names
|
80
|
+
base = @default_columns
|
81
|
+
base = @only_columns unless @only_columns.empty?
|
82
|
+
Array(base).collect(&:name) + Array(@included_columns).collect(&:name) - Array(@excepted_columns).collect(&:to_s)
|
83
|
+
end
|
84
|
+
|
85
|
+
def columns
|
86
|
+
usable_column_names.collect do |name|
|
87
|
+
@column_hash[name]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'row_group'
|
2
|
+
require 'hash_extensions'
|
3
|
+
|
4
|
+
module TablePrint
|
5
|
+
class Fingerprinter
|
6
|
+
def lift(columns, object)
|
7
|
+
@column_names_by_display_method = {}
|
8
|
+
columns.each { |c| @column_names_by_display_method[c.display_method] = c.name }
|
9
|
+
|
10
|
+
column_hash = display_methods_to_nested_hash(columns.collect(&:display_method))
|
11
|
+
|
12
|
+
hash_to_rows("", column_hash, object)
|
13
|
+
end
|
14
|
+
|
15
|
+
def hash_to_rows(prefix, hash, objects)
|
16
|
+
rows = []
|
17
|
+
|
18
|
+
# convert each object into its own row
|
19
|
+
Array(objects).each do |target|
|
20
|
+
row = populate_row(prefix, hash, target)
|
21
|
+
rows << row
|
22
|
+
|
23
|
+
# make a group and recurse for the columns we don't handle
|
24
|
+
groups = create_child_group(prefix, hash, target)
|
25
|
+
row.add_children(groups) unless groups.all? {|g| g.children.empty?}
|
26
|
+
end
|
27
|
+
|
28
|
+
rows
|
29
|
+
end
|
30
|
+
|
31
|
+
def populate_row(prefix, hash, target)
|
32
|
+
row = TablePrint::Row.new()
|
33
|
+
|
34
|
+
# populate a row with the columns we handle
|
35
|
+
cells = {}
|
36
|
+
handleable_columns(hash).each do |method|
|
37
|
+
display_method = (prefix == "" ? method : "#{prefix}.#{method}")
|
38
|
+
cell_value = method.call(target) if method.is_a? Proc
|
39
|
+
cell_value ||= target.send(method)
|
40
|
+
cells[@column_names_by_display_method[display_method]] = cell_value
|
41
|
+
end
|
42
|
+
|
43
|
+
row.set_cell_values(cells)
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_child_group(prefix, hash, target)
|
47
|
+
passable_columns(hash).collect do |name|
|
48
|
+
recursing_prefix = "#{prefix}#{'.' unless prefix == ''}#{name}"
|
49
|
+
group = RowGroup.new
|
50
|
+
group.add_children hash_to_rows(recursing_prefix, hash[name], target.send(name))
|
51
|
+
group
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def handleable_columns(hash)
|
56
|
+
# get the keys where the value is an empty hash
|
57
|
+
hash.select { |k, v| v == {} }.collect { |k, v| k }
|
58
|
+
end
|
59
|
+
|
60
|
+
def passable_columns(hash)
|
61
|
+
# get the keys where the value is not an empty hash
|
62
|
+
hash.select { |k, v| v != {} }.collect { |k, v| k }
|
63
|
+
end
|
64
|
+
|
65
|
+
def display_methods_to_nested_hash(display_methods)
|
66
|
+
extended_hash = {}.extend TablePrint::HashExtensions::ConstructiveMerge
|
67
|
+
|
68
|
+
# turn each column chain into a nested hash and add it to the output
|
69
|
+
display_methods.inject(extended_hash) do |hash, display_method|
|
70
|
+
hash.constructive_merge!(display_method_to_nested_hash(display_method))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def display_method_to_nested_hash(display_method)
|
75
|
+
hash = {}
|
76
|
+
|
77
|
+
return {display_method => {}} if display_method.is_a? Proc
|
78
|
+
|
79
|
+
display_method.split(".").inject(hash) do |hash_level, method|
|
80
|
+
hash_level[method] ||= {}
|
81
|
+
end
|
82
|
+
hash
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/formatter.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'config'
|
2
|
+
|
3
|
+
module TablePrint
|
4
|
+
class TimeFormatter
|
5
|
+
def initialize(time_format=nil)
|
6
|
+
@format = time_format
|
7
|
+
@format ||= TablePrint::Config.time_format
|
8
|
+
end
|
9
|
+
|
10
|
+
def format(value)
|
11
|
+
return value unless value.is_a? Time
|
12
|
+
value.strftime @format
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class NoNewlineFormatter
|
17
|
+
def format(value)
|
18
|
+
value.to_s.gsub(/\r\n/, "\n").gsub(/\n/, " ")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class FixedWidthFormatter
|
23
|
+
def initialize(width)
|
24
|
+
@width = width
|
25
|
+
end
|
26
|
+
|
27
|
+
def format(value)
|
28
|
+
"%-#{width}s" % truncate(value)
|
29
|
+
end
|
30
|
+
|
31
|
+
def width
|
32
|
+
[@width, TablePrint::Config.max_width].min
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def truncate(value)
|
37
|
+
return "" unless value
|
38
|
+
|
39
|
+
value = value.to_s
|
40
|
+
return value unless value.length > width
|
41
|
+
|
42
|
+
"#{value[0..width-4]}..."
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module TablePrint
|
2
|
+
module HashExtensions
|
3
|
+
module ConstructiveMerge
|
4
|
+
def constructive_merge(hash)
|
5
|
+
target = dup
|
6
|
+
|
7
|
+
hash.keys.each do |key|
|
8
|
+
if hash[key].is_a? Hash and self[key].is_a? Hash
|
9
|
+
target[key].extend ConstructiveMerge
|
10
|
+
target[key] = target[key].constructive_merge(hash[key])
|
11
|
+
next
|
12
|
+
end
|
13
|
+
|
14
|
+
target[key] = hash[key]
|
15
|
+
end
|
16
|
+
|
17
|
+
target
|
18
|
+
end
|
19
|
+
|
20
|
+
def constructive_merge!(hash)
|
21
|
+
target = self
|
22
|
+
|
23
|
+
hash.keys.each do |key|
|
24
|
+
if hash[key].is_a? Hash and self[key].is_a? Hash
|
25
|
+
target[key].extend ConstructiveMerge
|
26
|
+
target[key] = target[key].constructive_merge(hash[key])
|
27
|
+
next
|
28
|
+
end
|
29
|
+
|
30
|
+
target[key] = hash[key]
|
31
|
+
end
|
32
|
+
|
33
|
+
target
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|