table_print 0.2.3 → 1.0.0.rc3
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.
- 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
|