spodunk 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.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,20 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rspec"
10
+ gem "bundler"
11
+ gem "jeweler"
12
+ gem 'vcr'
13
+ gem 'webmock'
14
+ gem 'dotenv'
15
+ end
16
+
17
+ gem 'pry'
18
+ gem 'hashie'
19
+ gem 'google_drive'
20
+ gem 'activesupport', "~> 3.2.15"
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 dannguyen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ # Spodunk
2
+
3
+ A futile attempt to use spreadsheets as ad-hoc databases
4
+
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "spodunk"
18
+ gem.homepage = "http://github.com/dannguyen/spodunk"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{A quickie wrapper to use Google Spreadsheets as a database}
21
+ gem.description = %Q{Spodunk stands for "spreadsheet" + "podunk"}
22
+ gem.email = "dansonguyen@gmail.com"
23
+ gem.authors = ["dannguyen"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rspec/core'
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ task :default => :spec
40
+
41
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/examples/go.rb ADDED
@@ -0,0 +1,22 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'dotenv'
4
+ dotpath = File.expand_path File.join(File.dirname(__FILE__), '..', '.env')
5
+ # puts dotpath
6
+ Dotenv.load dotpath
7
+
8
+ require 'spodunk'
9
+ include Spodunk
10
+
11
+
12
+
13
+ @creds = {email: ENV['GDRIVE_TEST_EMAIL'], password: ENV['GDRIVE_TEST_PASSWORD']}
14
+ @url = ENV['GDRIVE_TEST_SPREADSHEET_URL']
15
+ @title = ENV['GDRIVE_TEST_WORKSHEET_TITLE']
16
+
17
+ @gd = Connection::GDrive.new(@creds, url: @url)
18
+ @gd.load_table('models')
19
+
20
+ @rows = @gd.table('models').rows
21
+
22
+
@@ -0,0 +1,79 @@
1
+ module Spodunk
2
+ module Connection
3
+ class Base
4
+ attr_reader :session, :spreadsheet
5
+
6
+ # the interface between Hash objects and Google Spreadsheets
7
+ def initialize(*args)
8
+ @tables = {}
9
+ end
10
+
11
+
12
+
13
+ def load_table(title)
14
+ unless title.nil?
15
+ @tables[title] = {}
16
+ @tables[title][:real] = real_t = fetch_real_table_object(title)
17
+ @tables[title][:podunk] = init_podunk_table(real_t, title: title)
18
+ end
19
+ end
20
+
21
+
22
+
23
+
24
+ def load_spreadsheet(*args);end
25
+
26
+ def real_tables(tid)
27
+ if t = @tables[tid]
28
+ return t[:real]
29
+ end
30
+ end
31
+
32
+ def tables(tid)
33
+ if t = @tables[tid]
34
+ return t[:podunk]
35
+ end
36
+ end
37
+
38
+ def table(tid)
39
+ tables(tid)
40
+ end
41
+
42
+ def save_row(table_id, row)
43
+ pt = tables(table_id)
44
+ row_idx = pt.real_row_index(row)
45
+
46
+ row.itemized_changes.each_pair do |col_idx, val|
47
+ save_cell(table_id, row_idx, col_idx, val)
48
+ end
49
+ end
50
+
51
+ def save_table(table_id)
52
+ pt = tables(table_id)
53
+
54
+ # expects itemized_changes to be {[2,3] => 4}
55
+ # with row_offset and col_offset already applied
56
+ pt.itemized_changes.each_pair do |(r, c), val|
57
+ save_cell(table_id, r, c, val)
58
+ end
59
+ end
60
+
61
+ # arr_xy is something like [2,3] (row 2, col 3)
62
+ # expects arr_xy to contain offset-applied row and col index
63
+ def save_cell(table_id, real_row_idx, real_col_idx, val)
64
+ # abstract
65
+ end
66
+
67
+
68
+
69
+
70
+
71
+ protected
72
+ def init_podunk_table(*args); end
73
+ def fetch_real_table_object(*args); end
74
+
75
+ end
76
+ end
77
+ end
78
+
79
+ require 'spodunk/connections/gdrive'
@@ -0,0 +1,63 @@
1
+ require 'google_drive'
2
+ module Spodunk
3
+ module Connection
4
+ class GDrive < Base
5
+
6
+ # the interface between Hash objects and Google Spreadsheets
7
+ def initialize(creds, sheet_opts={})
8
+ # old style auth
9
+ if creds[:email] && creds[:password]
10
+ @session = GoogleDrive.login creds[:email], creds[:password]
11
+ end
12
+ @spreadsheet = load_spreadsheet(sheet_opts)
13
+
14
+ super
15
+ end
16
+
17
+
18
+
19
+
20
+ # url is something like:
21
+ #
22
+ # https://docs.google.com/spreadsheet/ccc?key=xXcdHmAt3Q3D3lDx&usp=drive_web#gid=0
23
+ def load_spreadsheet(opts)
24
+ if opts[:url]
25
+ @session.spreadsheet_by_key parse_gdoc_url_for_key(opts[:url])
26
+ elsif opts[:key]
27
+ @session.spreadsheet_by_key(opts[:key])
28
+ end
29
+ end
30
+
31
+
32
+ # via: https://github.com/gimite/google-drive-ruby#how-to-use
33
+ def save_cell(table_id, real_row_idx, real_col_idx, val)
34
+ r_table = real_tables(table_id) # i.e. a Google Worksheet
35
+ r_table[real_row_idx, real_col_idx] = val
36
+
37
+ r_table.save
38
+ end
39
+
40
+
41
+ protected
42
+ # returns a string for key
43
+ def parse_gdoc_url_for_key(url)
44
+ u = URI.parse(url)
45
+
46
+ key = CGI.parse(u.query)['key'][0]
47
+ end
48
+
49
+
50
+ # sets up the table structure
51
+ def init_podunk_table(gsheet, opts={})
52
+ Table.new(gsheet.rows, opts)
53
+ end
54
+
55
+
56
+ def fetch_real_table_object(title)
57
+ @spreadsheet.worksheet_by_title(title)
58
+ end
59
+
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,139 @@
1
+ require 'hashie'
2
+
3
+ module Spodunk
4
+ class Row
5
+ attr_reader :mash, :original, :timestamp
6
+
7
+ delegate :values, :keys, :to => :mash
8
+ delegate :values, :to => :original, prefix: true
9
+ # expects vals and headers to be 1 to 1
10
+
11
+
12
+ def initialize(vals, headers)
13
+ slugged_headers = headers.map{|h| h.slugify}
14
+ @mash = Hashie::Mash.new(Hash[slugged_headers.zip(vals)])
15
+ @original = @mash.dup.freeze
16
+ @timestamp = Time.now
17
+ end
18
+
19
+
20
+ def changes(opts={})
21
+ offset = opts[:col_offset]
22
+ diff.inject({}) do |h, (k,v)|
23
+ x = offset ? mash_index(k) + offset : k
24
+ h[x] = v.last
25
+
26
+ h
27
+ end
28
+ end
29
+
30
+ # by default, spreadsheets are assumed to be numbered with columns
31
+ # starting from 1
32
+ def itemized_changes
33
+ changes(col_offset: 1)
34
+ end
35
+
36
+ # finds difference between @originals and @mash
37
+ def diff
38
+ dirty_diff(@original, @mash)
39
+ end
40
+
41
+ def clean?
42
+ diff.empty?
43
+ end
44
+
45
+ def dirty?
46
+ !clean?
47
+ end
48
+
49
+
50
+ def [](key)
51
+ @mash[mash_key(key)]
52
+ end
53
+
54
+ def []=(key, val)
55
+ @mash[mash_key(key)] = val
56
+ end
57
+
58
+ def method_missing(foo, *args, &blk)
59
+ foop = foo.to_s.match(/\w+(?==)?/).to_s
60
+ if @mash.keys.include?(foop)
61
+ @mash.send(foo, *args)
62
+ else
63
+ super
64
+ end
65
+ end
66
+
67
+ def respond_to?(foo, inc_private=false)
68
+ foop = foo.to_s.match(/\w+(?=$|=)/).to_s
69
+ if @mash.keys.include?(foop)
70
+ true
71
+ else
72
+ super
73
+ end
74
+ end
75
+
76
+
77
+ def attributes
78
+ @mash.stringify_keys
79
+ end
80
+
81
+ def assign_attributes(hsh)
82
+ new_atts = hsh.inject({}) do |h, (k, v)|
83
+ if has_attribute?(k)
84
+ # ignore attributes that aren't part of the model
85
+ h[mash_key(k)] = v
86
+ end
87
+
88
+ h
89
+ end
90
+
91
+ @mash.merge!( new_atts )
92
+ end
93
+
94
+
95
+ def has_attribute?(k)
96
+ attributes.keys.include?(mash_key(k))
97
+ end
98
+
99
+ private
100
+
101
+ # Since Rails diff is being deprecated
102
+ # this is a simple hack that DOES NOT do nested hashes
103
+ # and expects m1 and m2 to have EXACTLY the same stringified keys
104
+ #
105
+ # returns a hash with an array of differences
106
+ # {
107
+ # a: ['before', 'after'],
108
+ # b: [nil, 'now']
109
+ # }
110
+
111
+ def dirty_diff(h1, h2)
112
+ h1.stringify_keys.inject({}) do |h, (k, v1)|
113
+ v2 = h2[k]
114
+ h[k] = [v1, v2] if v1 != v2
115
+
116
+ h
117
+ end
118
+ end
119
+
120
+ # a method that converts integer vals or Strings to proper
121
+ # slugified keys
122
+ def mash_key(idx)
123
+ if idx.is_a?(Fixnum)
124
+ return keys[idx]
125
+ else
126
+ return idx.to_s.slugify
127
+ end
128
+ end
129
+
130
+ def mash_index(key)
131
+ if key.is_a?(Fixnum)
132
+ return key
133
+ else
134
+ return keys.index(key.to_s)
135
+ end
136
+ end
137
+
138
+ end
139
+ end
@@ -0,0 +1,104 @@
1
+ require 'active_support/all'
2
+
3
+ module Spodunk
4
+ class Table
5
+
6
+ attr_reader :headers, :rows, :title, :slug_headers,
7
+ :row_offset, :col_offset
8
+
9
+ def initialize(arr, opts={})
10
+ rs = arr.dup
11
+ @headers = rs.shift
12
+ @slug_headers = @headers.map{|h| h.slugify}
13
+ # rows get attributes as slugs
14
+ @rows = rs.map{ |r| Row.new(r, @slug_headers)}
15
+
16
+ @title = opts[:title]
17
+
18
+ # most spreadsheets start counting from 1
19
+ # and the first row is technically at index-2
20
+ # since headers is index-1
21
+ @row_offset = opts[:row_offset] || 2
22
+
23
+ # most spreadsheets begin column counting at 1
24
+ @col_offset = opts[:col_offset] || 1
25
+ end
26
+
27
+ def real_row_index(row)
28
+ if row.is_a?(Fixnum)
29
+ idx = row
30
+ else
31
+ idx = @rows.index(row)
32
+ end
33
+
34
+ return idx + @row_offset
35
+ end
36
+
37
+
38
+ def num_rows
39
+ @rows.count
40
+ end
41
+
42
+ def num_cols
43
+ @headers.count
44
+ end
45
+
46
+
47
+ def valid?
48
+ @slug_headers.uniq.count == num_cols
49
+ end
50
+
51
+ def clean_rows
52
+ @rows.reject{|r| r.dirty?}
53
+ end
54
+
55
+ def dirty_rows
56
+ @rows.select{|r| r.dirty?}
57
+ end
58
+
59
+ def clean?
60
+ dirty_rows.empty?
61
+ end
62
+
63
+ def dirty?
64
+ !clean?
65
+ end
66
+
67
+ # returns with 0-based index and column names as Strings
68
+ def changes(opts={})
69
+ row_offset = opts[:row_offset].to_i
70
+
71
+ dirty_rows.inject({}) do |h, row|
72
+ idx = @rows.index(row) + row_offset
73
+ h[idx] = row.changes(opts)
74
+
75
+ h
76
+ end
77
+ end
78
+
79
+ # returns format more specific for Google Worksheets
80
+ # with 1-based index and Hash with column indices rather than
81
+ # Strings
82
+ def offset_changes
83
+ changes(col_offset: @col_offset, row_offset: @row_offset)
84
+ end
85
+
86
+ # similar to offset_changes, except hash contains keys
87
+ # of [row, val] Arrays
88
+ def itemized_changes
89
+ offset_changes.inject({}) do |h, (row_num, col_hsh) |
90
+ col_hsh.each_pair do |col_num, val|
91
+ h[[row_num, col_num]] = val
92
+ end
93
+
94
+ h
95
+ end
96
+ end
97
+
98
+ # define unique ID field or concatenation
99
+
100
+
101
+
102
+
103
+ end
104
+ end
data/lib/spodunk.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'active_support/all'
2
+
3
+ module Spodunk
4
+ NULL_FIELD_VALUE = "!NULL"
5
+ end
6
+
7
+ require 'spodunk/connection'
8
+ require 'spodunk/table'
9
+ require 'spodunk/row'
10
+
11
+
12
+ class String
13
+ def slugify
14
+ self.underscore.parameterize.underscore
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'conveniences' do
4
+
5
+
6
+ describe 'slugify' do
7
+ it 'should underscore and strip' do
8
+ expect('Hey'.slugify).to eq 'hey'
9
+ expect('heyYou'.slugify).to eq 'hey_you'
10
+ expect('Hey you'.slugify).to eq 'hey_you'
11
+ expect('Hey ! !!! you! !'.slugify).to eq 'hey_you'
12
+ expect('hey_you'.slugify).to eq 'hey_you'
13
+ end
14
+ end
15
+
16
+ end
@@ -0,0 +1,147 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ describe "Spodunk::Row" do
5
+
6
+ let(:headers){ %w(Apples Oranges Blue-kiwi) }
7
+ let(:values){ [20, 9, 'none'] }
8
+
9
+ before{ @row = Row.new(values, headers)}
10
+
11
+ describe 'initialization' do
12
+ it 'should accept values and headers array and make a mash' do
13
+ expect(@row).to be_a Row
14
+ expect(@row.mash).to be_a Hashie::Mash
15
+ expect(@row.original).to be_a Hashie::Mash
16
+ end
17
+
18
+ it 'should delegate values and in the @mash' do
19
+ expect(@row.values).to eq values
20
+ end
21
+
22
+ it 'should delegate keys to @mash, but @mash has them slugified' do
23
+ expect(@row.keys).to eq headers.map{|h| h.slugify}
24
+ end
25
+
26
+ it 'should have method_missing apply to slugified headers' do
27
+ expect(@row.blue_kiwi).to eq 'none'
28
+ expect(@row.respond_to?(:oranges)).to be_true
29
+ end
30
+
31
+ it 'should have method_missing apply to slugified headers with assignment' do
32
+ @row.blue_kiwi = 'some'
33
+ expect(@row.blue_kiwi).to eq 'some'
34
+ expect(@row.respond_to?(:oranges=)).to be_true
35
+ end
36
+
37
+
38
+ it 'should have #original_values' do
39
+ expect(@row.original_values).to eq values
40
+ end
41
+
42
+ it 'should allow access by column index by delegating to @values' do
43
+ expect(@row[1]).to eq 9
44
+ end
45
+
46
+ it 'should be timestamped' do
47
+ expect(@row.timestamp).to be_within(10).of(Time.now)
48
+ end
49
+ end
50
+
51
+ context 'changes' do
52
+ before do
53
+ @row[0] = 20 # no change
54
+ @row[1] = 200
55
+ @row[2] = nil
56
+ end
57
+
58
+ describe 'dirty?' do
59
+ it 'should be true if anything has changed' do
60
+ @row[1] = 20
61
+ expect(@row).to be_dirty
62
+ end
63
+ end
64
+
65
+ describe 'diff' do
66
+ it 'should list all changes' do
67
+ expect(@row.diff).to eq({
68
+ 'oranges' => [9, 200],
69
+ 'blue_kiwi' => ['none', nil]
70
+ })
71
+ end
72
+ end
73
+
74
+ describe '#changes' do
75
+ it 'by default, returns a Hash header-names as keys' do
76
+ expect(@row.changes).to eq({
77
+ 'oranges' => 200,
78
+ 'blue_kiwi' => nil
79
+ })
80
+ end
81
+
82
+ it 'should take :offset option' do
83
+ expect(@row.changes(col_offset: 0)).to eq({
84
+ 1 => 200,
85
+ 2 => nil
86
+ }
87
+ )
88
+ end
89
+
90
+ it 'should have #itemized_changes as convenience' do
91
+ expect(@row.itemized_changes).to eq({
92
+ 2 => 200,
93
+ 3 => nil
94
+ }
95
+ )
96
+
97
+ expect(@row.itemized_changes).to eq @row.changes(col_offset: 1)
98
+ end
99
+ end
100
+ end
101
+
102
+ context '#attributes' do
103
+ let(:headers){ %w(Apples Oranges Blue-kiwi) }
104
+ let(:values){ [20, 9, 'none'] }
105
+
106
+ before{ @row = Row.new(values, headers)}
107
+
108
+ it 'should be the same as @mash, with string keys' do
109
+ expect(@row.attributes).to eq @row.mash.stringify_keys
110
+ end
111
+
112
+ describe '#has_attribute?' do
113
+ it 'checks on string equality' do
114
+ expect(@row.has_attribute?('Blue-kiwi')).to be_true
115
+ expect(@row.has_attribute?('blue_kiwi')).to be_true
116
+ expect(@row.has_attribute?('red_kiwi')).to be_false
117
+
118
+ # note, headers are slugified at the Table level
119
+ # not when we initialize rows manually
120
+ end
121
+ end
122
+
123
+
124
+ describe '#assign_attributes' do
125
+ it 'should modify the row' do
126
+ @row.assign_attributes('Blue-kiwi' => 5)
127
+ expect(@row).to be_dirty
128
+ expect(@row.attributes['blue_kiwi']).to eq 5
129
+ end
130
+
131
+ it 'should work with slugified attributes' do
132
+ @row.assign_attributes('blue_kiwi' => 'yay')
133
+
134
+ expect(@row.attributes['blue_kiwi']).to eq 'yay'
135
+ end
136
+
137
+ it 'should silently ignore non-existent attributes' do
138
+ @row.assign_attributes('nothing_here' => 99)
139
+ expect(@row.attributes['nothing_here']).to be_nil
140
+ expect(@row).not_to be_dirty
141
+ end
142
+ end
143
+ end
144
+
145
+
146
+
147
+ end
@@ -0,0 +1,142 @@
1
+ require 'spec_helper'
2
+
3
+ describe Spodunk::Table do
4
+
5
+ context 'basic ops' do
6
+ let(:rows){[
7
+ ['Hello', 'world', "It's me"],
8
+ ['a', 1, "99"]
9
+ ]}
10
+ before do
11
+ @table = Table.new(rows)
12
+ end
13
+
14
+ describe 'initialization by rows' do
15
+ it 'should accept array as first argument ' do
16
+ expect(@table).to be_a Table
17
+ end
18
+
19
+ it 'should not modify original array' do
20
+ expect(rows.count).to eq 2
21
+ end
22
+ end
23
+
24
+ describe 'rows and indicies' do
25
+ it 'should have basic counts' do
26
+ expect(@table.num_rows).to eq 1
27
+ expect(@table.num_cols).to eq 3
28
+ end
29
+
30
+ describe '#real_row_index' do
31
+ it 'by default has @row_offset of 2' do
32
+ expect(@table.row_offset).to eq 2
33
+ end
34
+
35
+ it 'by default has @col_offset of 1' do
36
+ expect(@table.col_offset).to eq 1
37
+ end
38
+
39
+ it 'uses @row_offset in real_row_index' do
40
+ expect(@table.real_row_index(0)).to eq 2
41
+ end
42
+
43
+ it 'can take in a Spodunk::Row as argument' do
44
+ row = @table.rows.first
45
+
46
+ expect(@table.real_row_index(row)).to eq 2
47
+ end
48
+ end
49
+ end
50
+
51
+ describe '@headers' do
52
+ it 'should by default be first row' do
53
+ expect(@table.headers).to eq rows.first
54
+ end
55
+ end
56
+
57
+ describe '@rows' do
58
+ it 'should convert all rows to SpreadtableConnection::Row' do
59
+ expect(@table.rows.all?{|r| r.is_a?(Row)})
60
+ end
61
+
62
+ it 'should same as source arr, minus headers' do
63
+ expect(@table.rows[0].values).to eq rows.last
64
+ end
65
+ end
66
+ end
67
+
68
+
69
+ describe 'validation' do
70
+ it 'should be valid? if all headers are sluggibly unique' do
71
+ table = Table.new([
72
+ ['hey you', 'hey- there'],
73
+ [1,2]
74
+ ])
75
+
76
+ expect(table).to be_valid
77
+ end
78
+
79
+ it 'should not allow headers with same sluggable name' do
80
+ table = Table.new([
81
+ ['hey you', 'hey-you '],
82
+ [1,2]
83
+ ])
84
+
85
+ expect(table).not_to be_valid
86
+ end
87
+ end
88
+
89
+ describe 'configuration' do
90
+
91
+ end
92
+
93
+ describe 'factory methods' do
94
+ describe 'initialization with a key' do
95
+
96
+
97
+ end
98
+ end
99
+
100
+
101
+ context 'dirtiness' do
102
+ let(:rows){[
103
+ ['Hello', 'world', "It's me"],
104
+ ['a', 1, "99"]
105
+ ]}
106
+
107
+ before do
108
+ @table = Table.new(rows)
109
+ @row = @table.rows.first
110
+ @row['world'] = 'changed'
111
+ end
112
+
113
+ it 'should be #dirty?' do
114
+ expect(@table).to be_dirty
115
+ end
116
+
117
+ describe '#changes' do
118
+ it 'should list changes' do
119
+ expect(@table.changes).to eq({
120
+ 0 => {'world' => 'changed'}
121
+ })
122
+ end
123
+
124
+ it '#offset_changes should list changes with 1-based index and col numbers' do
125
+ expect(@table.offset_changes).to eq({
126
+ 2 => { 2 => 'changed'}
127
+ })
128
+ end
129
+
130
+ it '#itemized_changes should be Hash with [row,col] as keys' do
131
+ expect(@table.itemized_changes).to eq({
132
+ [2,2] => 'changed'
133
+ })
134
+ end
135
+
136
+
137
+ end
138
+ end
139
+
140
+
141
+
142
+ end
@@ -0,0 +1,33 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'pry'
5
+ require 'spodunk'
6
+
7
+ include Spodunk
8
+
9
+ # Requires supporting files with custom matchers and macros, etc,
10
+ # in ./support/ and its subdirectories.
11
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
12
+
13
+
14
+
15
+
16
+ RSpec.configure do |config|
17
+
18
+ config.filter_run_excluding skip: true
19
+ config.run_all_when_everything_filtered = true
20
+ config.filter_run :focus => true
21
+
22
+ config.mock_with :rspec
23
+
24
+ # Use color in STDOUT
25
+ config.color_enabled = true
26
+
27
+ # Use color not only in STDOUT but also in pagers and files
28
+ config.tty = true
29
+
30
+ # Use the specified formatter
31
+ config.formatter = :documentation # :progress, :html, :textmate
32
+
33
+ end
data/spodunk.gemspec ADDED
@@ -0,0 +1,84 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "spodunk"
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["dannguyen"]
12
+ s.date = "2013-12-02"
13
+ s.description = "Spodunk stands for \"spreadsheet\" + \"podunk\""
14
+ s.email = "dansonguyen@gmail.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rspec",
22
+ "Gemfile",
23
+ "LICENSE.txt",
24
+ "README.md",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "examples/go.rb",
28
+ "lib/spodunk.rb",
29
+ "lib/spodunk/connection.rb",
30
+ "lib/spodunk/connections/gdrive.rb",
31
+ "lib/spodunk/row.rb",
32
+ "lib/spodunk/table.rb",
33
+ "spec/lib/convenience_spec.rb",
34
+ "spec/lib/row_spec.rb",
35
+ "spec/lib/table_spec.rb",
36
+ "spec/spec_helper.rb",
37
+ "spodunk.gemspec"
38
+ ]
39
+ s.homepage = "http://github.com/dannguyen/spodunk"
40
+ s.licenses = ["MIT"]
41
+ s.require_paths = ["lib"]
42
+ s.rubygems_version = "1.8.23"
43
+ s.summary = "A quickie wrapper to use Google Spreadsheets as a database"
44
+
45
+ if s.respond_to? :specification_version then
46
+ s.specification_version = 3
47
+
48
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
49
+ s.add_runtime_dependency(%q<pry>, [">= 0"])
50
+ s.add_runtime_dependency(%q<hashie>, [">= 0"])
51
+ s.add_runtime_dependency(%q<google_drive>, [">= 0"])
52
+ s.add_runtime_dependency(%q<activesupport>, ["~> 3.2.15"])
53
+ s.add_development_dependency(%q<rspec>, [">= 0"])
54
+ s.add_development_dependency(%q<bundler>, [">= 0"])
55
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
56
+ s.add_development_dependency(%q<vcr>, [">= 0"])
57
+ s.add_development_dependency(%q<webmock>, [">= 0"])
58
+ s.add_development_dependency(%q<dotenv>, [">= 0"])
59
+ else
60
+ s.add_dependency(%q<pry>, [">= 0"])
61
+ s.add_dependency(%q<hashie>, [">= 0"])
62
+ s.add_dependency(%q<google_drive>, [">= 0"])
63
+ s.add_dependency(%q<activesupport>, ["~> 3.2.15"])
64
+ s.add_dependency(%q<rspec>, [">= 0"])
65
+ s.add_dependency(%q<bundler>, [">= 0"])
66
+ s.add_dependency(%q<jeweler>, [">= 0"])
67
+ s.add_dependency(%q<vcr>, [">= 0"])
68
+ s.add_dependency(%q<webmock>, [">= 0"])
69
+ s.add_dependency(%q<dotenv>, [">= 0"])
70
+ end
71
+ else
72
+ s.add_dependency(%q<pry>, [">= 0"])
73
+ s.add_dependency(%q<hashie>, [">= 0"])
74
+ s.add_dependency(%q<google_drive>, [">= 0"])
75
+ s.add_dependency(%q<activesupport>, ["~> 3.2.15"])
76
+ s.add_dependency(%q<rspec>, [">= 0"])
77
+ s.add_dependency(%q<bundler>, [">= 0"])
78
+ s.add_dependency(%q<jeweler>, [">= 0"])
79
+ s.add_dependency(%q<vcr>, [">= 0"])
80
+ s.add_dependency(%q<webmock>, [">= 0"])
81
+ s.add_dependency(%q<dotenv>, [">= 0"])
82
+ end
83
+ end
84
+
metadata ADDED
@@ -0,0 +1,228 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spodunk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - dannguyen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-12-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: pry
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: hashie
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: google_drive
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: activesupport
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 3.2.15
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 3.2.15
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: bundler
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: jeweler
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: vcr
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ - !ruby/object:Gem::Dependency
143
+ name: webmock
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ - !ruby/object:Gem::Dependency
159
+ name: dotenv
160
+ requirement: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ! '>='
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ description: Spodunk stands for "spreadsheet" + "podunk"
175
+ email: dansonguyen@gmail.com
176
+ executables: []
177
+ extensions: []
178
+ extra_rdoc_files:
179
+ - LICENSE.txt
180
+ - README.md
181
+ files:
182
+ - .document
183
+ - .rspec
184
+ - Gemfile
185
+ - LICENSE.txt
186
+ - README.md
187
+ - Rakefile
188
+ - VERSION
189
+ - examples/go.rb
190
+ - lib/spodunk.rb
191
+ - lib/spodunk/connection.rb
192
+ - lib/spodunk/connections/gdrive.rb
193
+ - lib/spodunk/row.rb
194
+ - lib/spodunk/table.rb
195
+ - spec/lib/convenience_spec.rb
196
+ - spec/lib/row_spec.rb
197
+ - spec/lib/table_spec.rb
198
+ - spec/spec_helper.rb
199
+ - spodunk.gemspec
200
+ homepage: http://github.com/dannguyen/spodunk
201
+ licenses:
202
+ - MIT
203
+ post_install_message:
204
+ rdoc_options: []
205
+ require_paths:
206
+ - lib
207
+ required_ruby_version: !ruby/object:Gem::Requirement
208
+ none: false
209
+ requirements:
210
+ - - ! '>='
211
+ - !ruby/object:Gem::Version
212
+ version: '0'
213
+ segments:
214
+ - 0
215
+ hash: 1249785139266135960
216
+ required_rubygems_version: !ruby/object:Gem::Requirement
217
+ none: false
218
+ requirements:
219
+ - - ! '>='
220
+ - !ruby/object:Gem::Version
221
+ version: '0'
222
+ requirements: []
223
+ rubyforge_project:
224
+ rubygems_version: 1.8.23
225
+ signing_key:
226
+ specification_version: 3
227
+ summary: A quickie wrapper to use Google Spreadsheets as a database
228
+ test_files: []