tablesmith 0.1.0

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.
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