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 +5 -0
- data/.rspec +1 -0
- data/Gemfile +20 -0
- data/LICENSE.txt +20 -0
- data/README.md +4 -0
- data/Rakefile +41 -0
- data/VERSION +1 -0
- data/examples/go.rb +22 -0
- data/lib/spodunk/connection.rb +79 -0
- data/lib/spodunk/connections/gdrive.rb +63 -0
- data/lib/spodunk/row.rb +139 -0
- data/lib/spodunk/table.rb +104 -0
- data/lib/spodunk.rb +16 -0
- data/spec/lib/convenience_spec.rb +16 -0
- data/spec/lib/row_spec.rb +147 -0
- data/spec/lib/table_spec.rb +142 -0
- data/spec/spec_helper.rb +33 -0
- data/spodunk.gemspec +84 -0
- metadata +228 -0
data/.document
ADDED
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
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
|
data/lib/spodunk/row.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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: []
|