tablesmith 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0b2cd809d21c4144fbb28e472f12e4f77510817d
4
+ data.tar.gz: 664d3bb41153050b8476ac761e1b4063594c7327
5
+ SHA512:
6
+ metadata.gz: 9ef5411a7defaefa9dd1246d985a6a063479832836d41a48a1f715aa9922833f049163ccb2e3cf44b5679a1acf212bdfa7bac9c6776eefe6f752de0ecb0367f0
7
+ data.tar.gz: 972584036fd2a3a4208a4aa82c06e39686c5cab70c9e47b53594d95d52f9120f5da4ff2d6b98f14bd776ee681534062d77c0fc558d76695b1e7184545afdfd0c
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .bundle/
2
+ bin/
3
+ pkg/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --backtrace
2
+ --color
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.4
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+ source "http://geminabox.lsqa.net"
3
+
4
+ # Specify your gem's dependencies in tablesmith.gemspec
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,67 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ tablesmith (0.1.0)
5
+ text-table
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ remote: http://geminabox.lsqa.net/
10
+ specs:
11
+ activemodel (3.2.22.5)
12
+ activesupport (= 3.2.22.5)
13
+ builder (~> 3.0.0)
14
+ activerecord (3.2.22.5)
15
+ activemodel (= 3.2.22.5)
16
+ activesupport (= 3.2.22.5)
17
+ arel (~> 3.0.2)
18
+ tzinfo (~> 0.3.29)
19
+ activesupport (3.2.22.5)
20
+ i18n (~> 0.6, >= 0.6.4)
21
+ multi_json (~> 1.0)
22
+ arel (3.0.3)
23
+ builder (3.0.4)
24
+ coderay (1.1.1)
25
+ diff-lcs (1.3)
26
+ docile (1.1.5)
27
+ i18n (0.8.6)
28
+ json (2.1.0)
29
+ method_source (0.8.2)
30
+ multi_json (1.12.1)
31
+ pry (0.10.4)
32
+ coderay (~> 1.1.0)
33
+ method_source (~> 0.8.1)
34
+ slop (~> 3.4)
35
+ rake (10.5.0)
36
+ rspec (2.99.0)
37
+ rspec-core (~> 2.99.0)
38
+ rspec-expectations (~> 2.99.0)
39
+ rspec-mocks (~> 2.99.0)
40
+ rspec-core (2.99.2)
41
+ rspec-expectations (2.99.2)
42
+ diff-lcs (>= 1.1.3, < 2.0)
43
+ rspec-mocks (2.99.4)
44
+ simplecov (0.14.1)
45
+ docile (~> 1.1.0)
46
+ json (>= 1.8, < 3)
47
+ simplecov-html (~> 0.10.0)
48
+ simplecov-html (0.10.1)
49
+ slop (3.6.0)
50
+ sqlite3 (1.3.13)
51
+ text-table (1.2.4)
52
+ tzinfo (0.3.53)
53
+
54
+ PLATFORMS
55
+ ruby
56
+
57
+ DEPENDENCIES
58
+ activerecord (~> 3.0)
59
+ pry
60
+ rake (~> 10.0)
61
+ rspec (~> 2.0)
62
+ simplecov
63
+ sqlite3
64
+ tablesmith!
65
+
66
+ BUNDLED WITH
67
+ 1.15.1
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2012-2015 LivingSocial, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Tablesmith
2
+
3
+ Drop-in gem for console tables for Hash, Array and ActiveRecord.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'tablesmith'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install tablesmith
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Why Not #{other_gem}?
24
+
25
+ Happy to learn about something else already out there, but have struggled to find something
26
+ that doesn't require some sort of setup. I want drop-in ready-to-go table output for Hashes,
27
+ Arrays and ActiveRecord objects.
28
+
29
+ Other gems that I've tried out that are awesome and do much more than what Tablesmith does,
30
+ but don't seem to specialize in what I want.
31
+
32
+ - Hirb
33
+ - text-table
34
+ - table_print
35
+ - awesome_print
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'bundler/gem_tasks'
3
+
4
+ FileList['tasks/*.rake'].each { |task| load task }
5
+
6
+ task :default => :spec
7
+
8
+ desc 'Run the specs.'
9
+ RSpec::Core::RakeTask.new do |t|
10
+ t.pattern = '*_spec.rb'
11
+ end
data/lib/tablesmith.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'tablesmith/batch'
2
+ require 'tablesmith/active_record_source'
3
+ require 'tablesmith/hash_rows_source'
4
+ require 'tablesmith/version'
@@ -0,0 +1,97 @@
1
+ module Tablesmith::ActiveRecordSource
2
+ def convert_item_to_hash_row(item)
3
+ # TODO: reload ActiveRecords automagically
4
+ if item.respond_to? :serializable_hash
5
+ hash = item.serializable_hash(process_all_columns(serializable_options))
6
+ hash = fold_un_sourced_attributes_into_source_hash(first.class.name.underscore.to_sym, hash)
7
+ flatten_inner_hashes(hash)
8
+ else
9
+ super
10
+ end
11
+ end
12
+
13
+ def column_order
14
+ @columns.map(&:full_unaliased_name)
15
+ end
16
+
17
+ # allows overriding
18
+ def serializable_options
19
+ {}
20
+ end
21
+
22
+ # TODO: memoize
23
+ def process_all_columns(serializable_options)
24
+ @columns = []
25
+
26
+ process_columns(serializable_options, first.class)
27
+
28
+ include = serializable_options[:include]
29
+
30
+ # swiped from activemodel-3.2.17/lib/active_model/serialization.rb
31
+ unless include.is_a?(Hash)
32
+ include = Hash[Array.wrap(include).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
33
+ end
34
+
35
+ include.each do |association, opts|
36
+ ar_class = first.class.reflections[association].klass
37
+ process_columns(opts, ar_class)
38
+ end
39
+
40
+ serializable_options
41
+ end
42
+
43
+ def process_columns(serializable_options, ar_class)
44
+ only = serializable_options[:only]
45
+ ar_columns = ar_class.columns.map { |c| Tablesmith::Column.new(name: c.name.to_s, source: ar_class.name.underscore) }
46
+ @columns += ar_columns
47
+ de_alias_columns(only, ar_columns) if only
48
+ @columns += (serializable_options[:methods] || []).map do |meth_sym|
49
+ Tablesmith::Column.new(name: meth_sym.to_s, source: ar_class.name.underscore)
50
+ end
51
+ end
52
+
53
+ def de_alias_columns(only, ar_columns)
54
+ only.map! do |attr|
55
+ hits = ar_columns.select { |c| c.name =~ /#{attr.to_s.gsub(/_/, '.*')}/ }
56
+ if hits.present?
57
+ hit = hits[0] # TODO: support multiple hits
58
+ if attr != hit
59
+ hit.alias = attr
60
+ hit.name
61
+ else
62
+ attr
63
+ end
64
+ else
65
+ attr
66
+ end
67
+ end
68
+ end
69
+
70
+ def flatten_inner_hashes(hash)
71
+ new_hash = {}
72
+ stack = hash.each_pair.to_a
73
+ while ary = stack.shift
74
+ key, value = ary
75
+ if value.is_a?(Hash)
76
+ value.each_pair do |assoc_key, assoc_value|
77
+ new_hash["#{key}.#{assoc_key}"] = assoc_value.to_s
78
+ end
79
+ else
80
+ new_hash[key] = value
81
+ end
82
+ end
83
+ new_hash
84
+ end
85
+
86
+ # Top-level attributes aren't nested in their own hash by default, this
87
+ # normalizes the overall hash by grouping those together:
88
+ #
89
+ # converts {"name"=>"supplier", "account"=>{"name"=>'account'}}
90
+ # into {"supplier"=>{"name"=>'supplier'}, "account"=>{"name"=>'account'}}
91
+ def fold_un_sourced_attributes_into_source_hash(source_sym, hash)
92
+ new_hash = {}
93
+ new_hash[source_sym] = hash
94
+ hash.delete_if { |k, v| new_hash[k] = v if v.is_a?(Hash) }
95
+ new_hash
96
+ end
97
+ end
@@ -0,0 +1,145 @@
1
+ require 'text-table'
2
+
3
+ module Tablesmith
4
+ class Batch < Array
5
+ def method_missing(meth_id, *args)
6
+ count = 1
7
+ self.map do |t|
8
+ $stderr.print '.' if count.divmod(100)[1] == 0
9
+ count += 1
10
+ t.send(meth_id, *args)
11
+ end
12
+ end
13
+
14
+ # irb
15
+ def inspect
16
+ pretty_inspect
17
+ end
18
+
19
+ # Pry 0.9 calls this
20
+ def pretty_inspect
21
+ text_table.to_s
22
+ end
23
+
24
+ # Pry 0.10 eventually uses PP, and this is the PP way to provide custom output
25
+ def pretty_print(pp)
26
+ pp.text pretty_inspect
27
+ end
28
+
29
+ def text_table
30
+ return ['(empty)'].to_text_table if self.empty?
31
+
32
+ rows = self.map { |item| convert_item_to_hash_row(item) }.compact
33
+
34
+ normalize_keys(rows)
35
+
36
+ rows.map! do |row|
37
+ # this sort gives preference to column_order then falls back to alphabetic for leftovers.
38
+ # this is handy when columns auto-generate based on hash data.
39
+ row.sort do |a, b|
40
+ a_col_name, b_col_name = [a.first, b.first]
41
+ a_col_index, b_col_index = [column_order.index(a_col_name), column_order.index(b_col_name)]
42
+
43
+ if a_col_index.nil? && b_col_index.nil?
44
+ a_col_name <=> b_col_name
45
+ else
46
+ (a_col_index || 999) <=> (b_col_index || 999)
47
+ end
48
+ end
49
+ end
50
+
51
+ rows = create_headers(rows) + (rows.map { |r| r.map(&:last) })
52
+ rows.to_text_table
53
+ end
54
+
55
+ # override in subclass or mixin
56
+ def convert_item_to_hash_row(item)
57
+ item
58
+ end
59
+
60
+ # override in subclass or mixin
61
+ def column_order
62
+ []
63
+ end
64
+
65
+
66
+ # TODO: resolve with column_order
67
+ def columns
68
+ @columns
69
+ end
70
+
71
+ def create_headers(rows)
72
+ column_names = rows.first.map(&:first)
73
+ grouped_headers(column_names) + [apply_column_aliases(column_names), :separator]
74
+ end
75
+
76
+ def grouped_headers(column_names)
77
+ groups = Hash.new { |h, k| h[k] = 0 }
78
+ column_names.map! do |name|
79
+ group, col = name.to_s.split(/\./)
80
+ col, group = [group, ''] if col.nil?
81
+ groups[group] += 1
82
+ col
83
+ end
84
+ if groups.keys.length == 1 # TODO: add option to show group header row when only one exists
85
+ []
86
+ else
87
+ row = []
88
+ # this relies on Ruby versions where hash retains add order
89
+ groups.each_pair do |name, span|
90
+ row << {value: name, align: :center, colspan: span}
91
+ end
92
+ [row, :separator]
93
+ end
94
+ end
95
+
96
+ def apply_column_aliases(column_names)
97
+ column_names.map do |name|
98
+ instance = columns.detect { |ca| ca.name.to_s == name.to_s }
99
+ value = instance ? instance.display_name : name
100
+ {:value => value, :align => :center}
101
+ end
102
+ end
103
+
104
+ # not all resulting rows will have data in all columns, so make sure all rows pad out missing columns
105
+ def normalize_keys(rows)
106
+ all_keys = rows.map { |hash_row| hash_row.keys }.flatten.uniq
107
+ rows.map { |hash_row| all_keys.each { |key| hash_row[key] ||= '' } }
108
+ end
109
+ end
110
+
111
+ class Column
112
+ attr_accessor :source, :name, :alias
113
+
114
+ def initialize(attributes={})
115
+ @source = attributes.delete(:source)
116
+ @name = attributes.delete(:name)
117
+ @alias = attributes.delete(:alias)
118
+ end
119
+
120
+ def display_name
121
+ "#{@alias || @name}"
122
+ end
123
+
124
+ def full_unaliased_name
125
+ "#{@source ? "#{@source}." : ''}#{@name}"
126
+ end
127
+ end
128
+ end
129
+
130
+ class Array
131
+ def to_batch
132
+ b = Tablesmith::Batch.new(self)
133
+
134
+ if b.first && b.first.is_a?(ActiveRecord::Base)
135
+ b.extend Tablesmith::ActiveRecordSource
136
+ end
137
+
138
+ if b.first && b.first.is_a?(Hash)
139
+ b.extend Tablesmith::HashRowsSource
140
+ end
141
+
142
+ b
143
+ end
144
+ end
145
+
@@ -0,0 +1,63 @@
1
+ module Tablesmith::HashRowsSource
2
+ def text_table
3
+ build_columns if columns.nil?
4
+ super
5
+ end
6
+
7
+ def convert_item_to_hash_row(item)
8
+ flatten_hash_to_row(item, columns)
9
+ end
10
+
11
+ def flatten_hash_to_row(deep_hash, columns)
12
+ row = ActiveSupport::OrderedHash.new
13
+ columns.each do |col_or_hash|
14
+ value_from_hash(row, deep_hash, col_or_hash)
15
+ end
16
+ row
17
+ end
18
+
19
+ # TODO: no support for deep
20
+ def build_columns
21
+ @columns ||= []
22
+ self.map do |hash_row|
23
+ @columns << hash_row.keys.map { |k| Tablesmith::Column.new(name: k) }
24
+ end
25
+ @columns.flatten!
26
+ end
27
+
28
+ def value_from_hash(row, deep_hash, col_or_hash)
29
+ case col_or_hash
30
+ when Tablesmith::Column
31
+ row[col_or_hash.display_name] = deep_hash[col_or_hash.name]
32
+ when Hash
33
+ col_or_hash.each_pair do |sub_hash_key, cols_or_hash|
34
+ [cols_or_hash].flatten.each do |inner_col_or_hash|
35
+ value_from_hash(row, deep_hash[sub_hash_key], inner_col_or_hash)
36
+ end
37
+ end
38
+ else
39
+ nil
40
+ end
41
+ rescue => e
42
+ $stderr.puts "#{e.message}: #{col_or_hash}" if @debug
43
+ end
44
+
45
+ def hash_rows_to_text_table(hash_rows)
46
+ require 'text-table'
47
+
48
+ header_row = hash_rows.first.keys
49
+ table = []
50
+ table << header_row
51
+
52
+ hash_rows.each do |hash_row|
53
+ row = []
54
+ header_row.each do |header|
55
+ row << hash_row[header]
56
+ end
57
+ table << row
58
+ end
59
+
60
+ # Array addition from text-table
61
+ table.to_table(:first_row_is_head => true)
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ module Tablesmith
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,330 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'ActiveRecordSource' do
4
+ it 'outputs text table of multiple ActiveRecords' do
5
+ a = Person.new.tap { |c| c.first_name = 'A' }
6
+ b = Person.new.tap { |c| c.first_name = 'B' }
7
+ expected = <<-TABLE
8
+ +----+------------+-----------+-----+-------------------+
9
+ | id | first_name | last_name | age | custom_attributes |
10
+ +----+------------+-----------+-----+-------------------+
11
+ | | A | | | |
12
+ | | B | | | |
13
+ +----+------------+-----------+-----+-------------------+
14
+ TABLE
15
+ [a, b].to_batch.text_table.to_s.should == expected
16
+ end
17
+
18
+ it 'outputs ActiveRecord in column order' do
19
+ p = Person.create(:first_name => 'chris', :last_name => 'mo', :age => 43)
20
+ expected = <<-TABLE
21
+ +----+------------+-----------+-----+-------------------+
22
+ | id | first_name | last_name | age | custom_attributes |
23
+ +----+------------+-----------+-----+-------------------+
24
+ | 1 | chris | mo | 43 | |
25
+ +----+------------+-----------+-----+-------------------+
26
+ TABLE
27
+ [p].to_batch.text_table.to_s.should == expected
28
+ end
29
+
30
+ it 'handles custom serialization options in batch' do
31
+ p = Person.create(:first_name => 'chrismo', :age => 43)
32
+
33
+ expected = <<-TABLE
34
+ +------------+-----+-----------+
35
+ | first_name | age | year_born |
36
+ +------------+-----+-----------+
37
+ | chrismo | 43 | 1971 |
38
+ +------------+-----+-----------+
39
+ TABLE
40
+ b = [p].to_batch
41
+
42
+ def b.serializable_options
43
+ {:only => [:first_name, :age], :methods => [:year_born]}
44
+ end
45
+
46
+ b.text_table.to_s.should == expected
47
+ end
48
+
49
+ it 'handles column name partials' do
50
+ p = Person.create(:first_name => 'chris', :last_name => 'mo', :age => 43)
51
+ expected = <<-TABLE
52
+ +-------+------+-----+
53
+ | first | last | age |
54
+ +-------+------+-----+
55
+ | chris | mo | 43 |
56
+ +-------+------+-----+
57
+ TABLE
58
+ b = [p].to_batch
59
+
60
+ def b.serializable_options
61
+ {:only => [:first, :last, :age]}
62
+ end
63
+
64
+ b.text_table.to_s.should == expected
65
+ end
66
+
67
+ it 'handles column name partials across words' do
68
+ p = Person.create(:first_name => 'chris', :last_name => 'mo', :age => 43)
69
+ expected = <<-TABLE
70
+ +--------+--------+-----+
71
+ | f_name | l_name | age |
72
+ +--------+--------+-----+
73
+ | chris | mo | 43 |
74
+ +--------+--------+-----+
75
+ TABLE
76
+ b = [p].to_batch
77
+
78
+ def b.serializable_options
79
+ {:only => [:f_name, :l_name, :age]}
80
+ end
81
+
82
+ b.text_table.to_s.should == expected
83
+ end
84
+
85
+ it 'handles explicit column aliases' do
86
+ p = Person.create(:first_name => 'chris', :last_name => 'mo', :age => 43)
87
+ expected = <<-TABLE
88
+ +---------------+----------+-----+
89
+ | primer_nombre | apellido | age |
90
+ +---------------+----------+-----+
91
+ | chris | mo | 43 |
92
+ +---------------+----------+-----+
93
+ TABLE
94
+ b = [p].to_batch
95
+
96
+ def b.columns
97
+ [Tablesmith::Column.new(name: :first_name, alias: :primer_nombre),
98
+ Tablesmith::Column.new(name: :last_name, alias: :apellido)]
99
+ end
100
+
101
+ def b.serializable_options
102
+ {:only => [:first_name, :last_name, :age]}
103
+ end
104
+
105
+ b.text_table.to_s.should == expected
106
+ end
107
+
108
+ it 'handles associations without aliases' do
109
+ s = Supplier.create(name: 'supplier')
110
+ s.account = Account.create(name: 'account', tax_identification_number: '123456')
111
+ b = [s].to_batch
112
+
113
+ def b.serializable_options
114
+ {:only => [:name], :include => {:account => {:only => [:name, :tax_identification_number]}}}
115
+ end
116
+
117
+ expected = <<-TABLE
118
+ +----------+---------+---------------------------+
119
+ | supplier | account |
120
+ +----------+---------+---------------------------+
121
+ | name | name | tax_identification_number |
122
+ +----------+---------+---------------------------+
123
+ | supplier | account | 123456 |
124
+ +----------+---------+---------------------------+
125
+ TABLE
126
+
127
+ b.text_table.to_s.should == expected
128
+ end
129
+
130
+ it 'handles associations with aliases' do
131
+ s = Supplier.create(name: 'supplier')
132
+ s.account = Account.create(name: 'account', tax_identification_number: '123456')
133
+ b = [s].to_batch
134
+
135
+ def b.serializable_options
136
+ {:only => [:name], :include => {:account => {:only => [:name, :tax_id]}}}
137
+ end
138
+
139
+ expected = <<-TABLE
140
+ +----------+---------+--------+
141
+ | supplier | account |
142
+ +----------+---------+--------+
143
+ | name | name | tax_id |
144
+ +----------+---------+--------+
145
+ | supplier | account | 123456 |
146
+ +----------+---------+--------+
147
+ TABLE
148
+
149
+ b.text_table.to_s.should == expected
150
+ end
151
+
152
+ it 'retains serializable_options ordering'
153
+
154
+ it 'supports multiple associations'
155
+
156
+ it 'supports nested associations'
157
+
158
+ # may need/want to handle the hash resulting from an association differently from the hash resulting from a method/attr
159
+ it 'supports field with hash contents' do
160
+ p = Person.create(first_name: 'chrismo', custom_attributes: {skills: {instrument: 'piano', style: 'jazz'}})
161
+ b = [p].to_batch
162
+
163
+ a = format_ids([p.id])[0]
164
+ expected = <<-TABLE
165
+ +----+------------+-----------+-----+----------------------------------------+
166
+ | person | custom_attributes |
167
+ +----+------------+-----------+-----+----------------------------------------+
168
+ | id | first_name | last_name | age | skills |
169
+ +----+------------+-----------+-----+----------------------------------------+
170
+ |#{a}| chrismo | | | {:instrument=>"piano", :style=>"jazz"} |
171
+ +----+------------+-----------+-----+----------------------------------------+
172
+ TABLE
173
+
174
+ b.text_table.to_s.should == expected
175
+ end
176
+
177
+ it 'supports multiple rows with different column counts' do
178
+ p2 = Person.create(first_name: 'romer', custom_attributes: {instrument: 'kazoo'})
179
+ p1 = Person.create(first_name: 'chrismo', custom_attributes: {instrument: 'piano', style: 'jazz'})
180
+ p3 = Person.create(first_name: 'glv', custom_attributes: {})
181
+ batch = [p2, p1, p3].to_batch
182
+
183
+ a, b, c = format_ids([p2.id, p1.id, p3.id])
184
+
185
+ expected = <<-TABLE
186
+ +----+------------+-----------+-----+------------+-----------+
187
+ | person | custom_attributes |
188
+ +----+------------+-----------+-----+------------+-----------+
189
+ | id | first_name | last_name | age | instrument | style |
190
+ +----+------------+-----------+-----+------------+-----------+
191
+ |#{a}| romer | | | kazoo | |
192
+ |#{b}| chrismo | | | piano | jazz |
193
+ |#{c}| glv | | | | |
194
+ +----+------------+-----------+-----+------------+-----------+
195
+ TABLE
196
+
197
+ batch.text_table.to_s.should == expected
198
+ end
199
+
200
+ it 'supports consistent ordering of dynamic columns' do
201
+ p1 = Person.create(first_name: 'chrismo', custom_attributes: {instrument: 'piano', style: 'jazz'})
202
+ p2 = Person.create(first_name: 'romer', custom_attributes: {hobby: 'games'})
203
+ batch = [p1, p2].to_batch
204
+
205
+ a, b = format_ids([p1.id, p2.id])
206
+
207
+ expected = <<-TABLE
208
+ +----+------------+-----------+-----+--------+------------+--------+
209
+ | person | custom_attributes |
210
+ +----+------------+-----------+-----+--------+------------+--------+
211
+ | id | first_name | last_name | age | hobby | instrument | style |
212
+ +----+------------+-----------+-----+--------+------------+--------+
213
+ |#{a}| chrismo | | | | piano | jazz |
214
+ |#{b}| romer | | | games | | |
215
+ +----+------------+-----------+-----+--------+------------+--------+
216
+ TABLE
217
+
218
+ batch.text_table.to_s.should == expected
219
+ end
220
+
221
+ it 'handles AR instance without an association present' do
222
+ s = Supplier.create(name: 'supplier')
223
+ b = [s].to_batch
224
+
225
+ def b.serializable_options
226
+ {:only => [:name], :include => {:account => {:only => [:name, :tax_id]}}}
227
+ end
228
+
229
+ expected = <<-TABLE
230
+ +----------+
231
+ | name |
232
+ +----------+
233
+ | supplier |
234
+ +----------+
235
+ TABLE
236
+
237
+ b.text_table.to_s.should == expected
238
+ end
239
+
240
+ it 'properly groups when original columns not sequential' do
241
+ s2 = Supplier.create(name: 'sup. two', custom_attributes: {a: 1})
242
+
243
+ def s2.foo
244
+ ''
245
+ end
246
+
247
+ b = [s2].to_batch
248
+
249
+ # methods need Columns as well
250
+ def b.serializable_options
251
+ {:only => [:name, :custom_attributes], :methods => [:foo]}
252
+ end
253
+
254
+ expected = <<-TABLE
255
+ +----------+------+-------------------+
256
+ | supplier | custom_attributes |
257
+ +----------+------+-------------------+
258
+ | name | foo | a |
259
+ +----------+------+-------------------+
260
+ | sup. two | | 1 |
261
+ +----------+------+-------------------+
262
+ TABLE
263
+
264
+ b.text_table.to_s.should == expected
265
+ end
266
+
267
+ it 'supports one to many association' do
268
+ p = Parent.create(name: 'parent')
269
+ c = Child.create(name: 'child', parent: p)
270
+
271
+ b = [p].to_batch
272
+
273
+ # little weird looking at this point, but at least not broken
274
+ expected = <<-TABLE
275
+ +----+--------+-------------------+---------------------+
276
+ | id | name | custom_attributes | children |
277
+ +----+--------+-------------------+---------------------+
278
+ | 1 | parent | | [{"name"=>"child"}] |
279
+ +----+--------+-------------------+---------------------+
280
+ TABLE
281
+
282
+ def b.serializable_options
283
+ {:include => {:children => {:only => [:name]}}}
284
+ end
285
+
286
+ b.text_table.to_s.should == expected
287
+ end
288
+
289
+ def format_ids(ary)
290
+ ary.map {|value| " #{value.to_s.ljust(3)}" }
291
+ end
292
+
293
+ describe 'fold un-sourced attributes into source hash' do
294
+ let(:obj) { Object.new.extend Tablesmith::ActiveRecordSource }
295
+
296
+ it 'should handle simple hash' do
297
+ obj.fold_un_sourced_attributes_into_source_hash(:foo, {a: 1, b: 2}).should == {foo: {a: 1, b: 2}}
298
+ end
299
+
300
+ it 'should handle nested hashes' do
301
+ before = {'name' => 'chris', account: {'name' => 'account_name'}}
302
+ expected = {foo: {'name' => 'chris'}, account: {'name' => 'account_name'}}
303
+ obj.fold_un_sourced_attributes_into_source_hash(:foo, before).should == expected
304
+ end
305
+
306
+ it 'should handle deep nested hashes' do
307
+ before = {'name' => 'chris', account: {'id' => {'name' => 'account_name', 'number' => 123456}}}
308
+ expected = {foo: {'name' => 'chris'}, account: {'id' => {'name' => 'account_name', 'number' => 123456}}}
309
+ obj.fold_un_sourced_attributes_into_source_hash(:foo, before).should == expected
310
+ end
311
+ end
312
+
313
+ describe 'flatten_inner_hashes' do
314
+ let(:obj) { Object.new.extend Tablesmith::ActiveRecordSource }
315
+
316
+ it 'should flatten inner hash' do
317
+ before = {foo: {'name' => 'chris'}, account: {'name' => 'account_name'}}
318
+ expected = {'foo.name' => 'chris', 'account.name' => 'account_name'}
319
+
320
+ obj.flatten_inner_hashes(before).should == expected
321
+ end
322
+
323
+ it 'should to_s deep nested hashes' do
324
+ before = {foo: {'name' => 'chris'}, account: {'id' => {'name' => 'account_name', 'number' => 123456}}}
325
+ expected = {'foo.name' => 'chris', "account.id" => "{\"name\"=>\"account_name\", \"number\"=>123456}"}
326
+
327
+ obj.flatten_inner_hashes(before).should == expected
328
+ end
329
+ end
330
+ end
@@ -0,0 +1 @@
1
+ # Want stuff to work with plain Arrays
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ include Tablesmith
4
+
5
+ describe Batch do
6
+ it 'should subclass array' do
7
+ b = Batch.new
8
+ b.length.should == 0
9
+ b << 1
10
+ b << 'a'
11
+ b[0].should == 1
12
+ b[1].should == 'a'
13
+ b.class.should == Batch
14
+ end
15
+
16
+ it 'should pass unmatched Array messages to all items' do
17
+ b = Batch.new
18
+ b.length.should == 0
19
+ b << 1
20
+ b << '2'
21
+ b.to_i.should == [1, 2]
22
+ end
23
+
24
+ it 'should handle empty Array' do
25
+ expected = <<-TEXT
26
+ +---------+
27
+ | (empty) |
28
+ +---------+
29
+ TEXT
30
+ [].to_batch.text_table.to_s.should == expected
31
+ end
32
+ end
data/spec/fixtures.rb ADDED
@@ -0,0 +1,70 @@
1
+ require 'active_record'
2
+
3
+ ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
4
+
5
+ class Person < ActiveRecord::Base
6
+ connection.create_table table_name, :force => true do |t|
7
+ t.string :first_name
8
+ t.string :last_name
9
+ t.integer :age
10
+ t.text :custom_attributes
11
+ end
12
+
13
+ def year_born
14
+ Time.local(2014, 1, 1).year - self.age
15
+ end
16
+ end
17
+
18
+ class Parent < ActiveRecord::Base
19
+ has_many :children
20
+
21
+ connection.create_table table_name, :force => true do |t|
22
+ t.string :name
23
+ t.text :custom_attributes
24
+ end
25
+ end
26
+
27
+ class Child < ActiveRecord::Base
28
+ belongs_to :parent
29
+
30
+ connection.create_table table_name, :force => true do |t|
31
+ t.integer :parent_id
32
+ t.string :name
33
+ end
34
+ end
35
+
36
+ class Supplier < ActiveRecord::Base
37
+ has_one :account
38
+ has_one :account_history, :through => :account
39
+
40
+ accepts_nested_attributes_for :account, :account_history
41
+
42
+ connection.create_table table_name, :force => true do |t|
43
+ t.integer :account_id
44
+ t.integer :account_history_id
45
+ t.string :name
46
+ t.text :custom_attributes
47
+ end
48
+ end
49
+
50
+ class Account < ActiveRecord::Base
51
+ belongs_to :supplier
52
+ has_one :account_history
53
+
54
+ accepts_nested_attributes_for :account_history
55
+
56
+ connection.create_table table_name, :force => true do |t|
57
+ t.integer :supplier_id
58
+ t.string :name
59
+ t.integer :tax_identification_number
60
+ end
61
+ end
62
+
63
+ class AccountHistory < ActiveRecord::Base
64
+ belongs_to :account
65
+
66
+ connection.create_table table_name, :force => true do |t|
67
+ t.integer :account_id
68
+ t.integer :credit_rating
69
+ end
70
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'HashRowsSource' do
4
+ it 'outputs text table of simple hash row with default columns' do
5
+ expected = <<-TABLE
6
+ +---+---+
7
+ | a | b |
8
+ +---+---+
9
+ | 1 | 2 |
10
+ +---+---+
11
+ TABLE
12
+ [{a: 1, b: 2}].to_batch.text_table.to_s.should == expected
13
+ end
14
+
15
+ it 'outputs text table of mixed columns hash rows with default columns' do
16
+ expected = <<-TABLE
17
+ +---+---+---+
18
+ | a | b | c |
19
+ +---+---+---+
20
+ | 1 | 2 | |
21
+ | 2 | | ! |
22
+ +---+---+---+
23
+ TABLE
24
+ [
25
+ {a: 1, b: 2},
26
+ {a: 2, c: '!'}
27
+ ].to_batch.text_table.to_s.should == expected
28
+ end
29
+
30
+ it 'outputs text table of deep hash rows with defined columns' do
31
+ expected = <<-TABLE
32
+ +---+---+---+
33
+ | | b |
34
+ +---+---+---+
35
+ | a | c | d |
36
+ +---+---+---+
37
+ | 1 | 2 | 2 |
38
+ +---+---+---+
39
+ TABLE
40
+ b = [{a: 1, b: {c: 2, d: 2}}].to_batch
41
+ def b.columns
42
+ [
43
+ Column.new(name: :a),
44
+ {b: [
45
+ Column.new(name: :c)
46
+ ]}
47
+ ]
48
+ end
49
+ # this would be nice. Payments has some code along these lines for BraintreeBatch? or some ActiveRecordSource re-use?
50
+ # b.text_table.to_s.should == expected
51
+ pending
52
+ end
53
+
54
+ it 'outputs text table of deep hash rows with default columns'
55
+ end
data/spec/hash_spec.rb ADDED
@@ -0,0 +1 @@
1
+ # Want stuff to work with a plain Hash
@@ -0,0 +1,3 @@
1
+ require File.expand_path('../../lib/tablesmith', __FILE__)
2
+
3
+ require File.expand_path('../fixtures', __FILE__)
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'tablesmith/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'tablesmith'
8
+ gem.version = Tablesmith::VERSION
9
+ gem.authors = ['chrismo']
10
+ gem.email = ['chrismo@clabs.org']
11
+ gem.description = %q{Minimal console table}
12
+ gem.summary = %q{Minimal console table}
13
+ gem.homepage = 'http://github.com/livingsocial/tablesmith'
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ['lib']
19
+
20
+ gem.add_dependency 'text-table'
21
+
22
+ gem.add_development_dependency 'activerecord', '~> 3.0'
23
+ gem.add_development_dependency 'pry'
24
+ gem.add_development_dependency 'rake', '~> 10.0'
25
+ gem.add_development_dependency 'rspec', '~> 2.0'
26
+ gem.add_development_dependency 'simplecov'
27
+ gem.add_development_dependency 'sqlite3'
28
+ end
metadata ADDED
@@ -0,0 +1,169 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tablesmith
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - chrismo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-07-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: text-table
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sqlite3
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Minimal console table
112
+ email:
113
+ - chrismo@clabs.org
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".rspec"
120
+ - ".ruby-version"
121
+ - Gemfile
122
+ - Gemfile.lock
123
+ - LICENSE
124
+ - README.md
125
+ - Rakefile
126
+ - lib/tablesmith.rb
127
+ - lib/tablesmith/active_record_source.rb
128
+ - lib/tablesmith/batch.rb
129
+ - lib/tablesmith/hash_rows_source.rb
130
+ - lib/tablesmith/version.rb
131
+ - spec/active_record_batch_spec.rb
132
+ - spec/array_spec.rb
133
+ - spec/batch_spec.rb
134
+ - spec/fixtures.rb
135
+ - spec/hash_rows_batch_spec.rb
136
+ - spec/hash_spec.rb
137
+ - spec/spec_helper.rb
138
+ - tablesmith.gemspec
139
+ homepage: http://github.com/livingsocial/tablesmith
140
+ licenses: []
141
+ metadata: {}
142
+ post_install_message:
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubyforge_project:
158
+ rubygems_version: 2.6.12
159
+ signing_key:
160
+ specification_version: 4
161
+ summary: Minimal console table
162
+ test_files:
163
+ - spec/active_record_batch_spec.rb
164
+ - spec/array_spec.rb
165
+ - spec/batch_spec.rb
166
+ - spec/fixtures.rb
167
+ - spec/hash_rows_batch_spec.rb
168
+ - spec/hash_spec.rb
169
+ - spec/spec_helper.rb