sheet_mapper 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +55 -0
- data/Rakefile +8 -0
- data/example.rb +53 -0
- data/example2.rb +30 -0
- data/lib/core_ext/hash_ext.rb +234 -0
- data/lib/core_ext/object_ext.rb +114 -0
- data/lib/sheet_mapper.rb +13 -0
- data/lib/sheet_mapper/base.rb +79 -0
- data/lib/sheet_mapper/collection.rb +47 -0
- data/lib/sheet_mapper/spreadsheet.rb +21 -0
- data/lib/sheet_mapper/version.rb +3 -0
- data/sheet_mapper.gemspec +28 -0
- data/test/base_test.rb +56 -0
- data/test/collection_test.rb +85 -0
- data/test/spreadsheet_test.rb +42 -0
- data/test/test_config.rb +12 -0
- data/test/test_helpers/mini_shoulda.rb +84 -0
- metadata +177 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# SheetMapper
|
2
|
+
|
3
|
+
SheetMapper is about taking a google spreadsheet and converting a set of data rows into ruby objects.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Setup in Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# Gemfile
|
11
|
+
|
12
|
+
gem 'sheet_mapper'
|
13
|
+
```
|
14
|
+
|
15
|
+
and then `require 'sheet_mapper'` and you are done!
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
First, define yourself an object mapper:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
class SomeMapper < SheetMapper::Base
|
23
|
+
# Defines each column for a row and maps each column to an attribute
|
24
|
+
columns :foo, :bar, :baz
|
25
|
+
|
26
|
+
# Defines the condition for a row to be considered valid
|
27
|
+
def valid_row?
|
28
|
+
self[:foo].present?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Convert is_notable column to a boolean from raw string
|
32
|
+
# Any methods named after a column will override the default value
|
33
|
+
def is_notable
|
34
|
+
!!self[:bar].match(/true/i)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
This describes the column mappings and transformations to turn a spreadsheet row into a ruby object. Then you can use
|
40
|
+
a mapper within a worksheet collection:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
sheet = SheetMapper::Worksheet.new(:mapper => SomeMapper, :key => 'k', :login => 'u', :password => 'p')
|
44
|
+
collection = sheet.find_collection_by_title('title')
|
45
|
+
records = collection.each do |record|
|
46
|
+
p record.attributes
|
47
|
+
# => { :foo => "...", :bar => false, ... }
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
You can then work with the objects within the collection and access their attributes.
|
52
|
+
|
53
|
+
## Contributors
|
54
|
+
|
55
|
+
SheetMapper was created by [Nathan Esquenazi](http://github.com/nesquena) at Miso in 2012.
|
data/Rakefile
ADDED
data/example.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
class BubbleMapper < SheetMapper::Base
|
4
|
+
attributes :offset_seconds, :body, :link_url, :category
|
5
|
+
end
|
6
|
+
|
7
|
+
sheet = SheetMapper::Worksheet.new(:mapper => BubbleMapper, :key => 'sheet_key', :login => 'user', :password => 'pass')
|
8
|
+
collection = sheet.find_collection_by_title('title')
|
9
|
+
bubbles = collection.each do |bubble|
|
10
|
+
p bubble.to_hash
|
11
|
+
end
|
12
|
+
|
13
|
+
=end
|
14
|
+
|
15
|
+
require 'rubygems'
|
16
|
+
require 'active_support/all'
|
17
|
+
require 'google_spreadsheet'
|
18
|
+
require File.expand_path('lib/sheet_mapper')
|
19
|
+
|
20
|
+
class BubbleMapper < SheetMapper::Base
|
21
|
+
columns :offset_seconds, :is_notable, :category, :body, :image_url, :link_text, :link_url
|
22
|
+
|
23
|
+
def valid_row?
|
24
|
+
self[:body].present? && @pos > 7
|
25
|
+
end
|
26
|
+
|
27
|
+
def offset_seconds
|
28
|
+
return unless self[:offset_seconds].strip =~ /^[\d\:]+$/ # Only return offset if valid digits
|
29
|
+
offset = self[:offset_seconds].strip.split(':')
|
30
|
+
(offset[0].to_i * 60) + offset[1].to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
# Convert is_notable to boolean
|
34
|
+
def is_notable
|
35
|
+
self[:is_notable].to_s.match(/true/i).present?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
sheet = SheetMapper::Spreadsheet.new(:mapper => BubbleMapper, :key => ENV['SHEET_KEY'], :login => ENV['SHEET_LOGIN'], :password => ENV['SHEET_PASS'])
|
40
|
+
collection = sheet.find_collection_by_title('s2e5')
|
41
|
+
|
42
|
+
media_id = collection.cell(2, 2)
|
43
|
+
season_num = collection.cell(3, 2)
|
44
|
+
episode_num = collection.cell(4, 2)
|
45
|
+
user_id = collection.cell(5, 2)
|
46
|
+
duration = collection.cell(6, 2)
|
47
|
+
|
48
|
+
puts "Media: #{media_id}, User: #{user_id}"
|
49
|
+
|
50
|
+
bubbles = collection.each do |bubble|
|
51
|
+
p bubble.attributes
|
52
|
+
end
|
53
|
+
|
data/example2.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'active_support/all'
|
3
|
+
require 'google_spreadsheet'
|
4
|
+
require File.expand_path('lib/sheet_mapper')
|
5
|
+
|
6
|
+
class GradeMapper < SheetMapper::Base
|
7
|
+
columns :topic, :grade, :score
|
8
|
+
|
9
|
+
def valid_row?
|
10
|
+
self[:topic].present? && @pos > 3
|
11
|
+
end
|
12
|
+
|
13
|
+
# Convert is_notable to boolean
|
14
|
+
def score
|
15
|
+
self[:score].to_i
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
sheet = SheetMapper::Spreadsheet.new(:mapper => GradeMapper, :key => ENV['SHEET_KEY'], :login => ENV['SHEET_LOGIN'], :password => ENV['SHEET_PASS'])
|
20
|
+
collection = sheet.find_collection_by_title('data')
|
21
|
+
|
22
|
+
name = collection.cell(1, 2)
|
23
|
+
age = collection.cell(2, 2)
|
24
|
+
|
25
|
+
puts "Name: #{name}, Age: #{age}"
|
26
|
+
|
27
|
+
bubbles = collection.each do |bubble|
|
28
|
+
p bubble.attributes
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,234 @@
|
|
1
|
+
class Hash
|
2
|
+
# Return a new hash with all keys converted to strings.
|
3
|
+
def stringify_keys
|
4
|
+
dup.stringify_keys!
|
5
|
+
end
|
6
|
+
|
7
|
+
# Destructively convert all keys to strings.
|
8
|
+
def stringify_keys!
|
9
|
+
keys.each do |key|
|
10
|
+
self[key.to_s] = delete(key)
|
11
|
+
end
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
# Return a new hash with all keys converted to symbols, as long as
|
16
|
+
# they respond to +to_sym+.
|
17
|
+
def symbolize_keys
|
18
|
+
dup.symbolize_keys!
|
19
|
+
end
|
20
|
+
|
21
|
+
# Destructively convert all keys to symbols, as long as they respond
|
22
|
+
# to +to_sym+.
|
23
|
+
def symbolize_keys!
|
24
|
+
keys.each do |key|
|
25
|
+
self[(key.to_sym rescue key) || key] = delete(key)
|
26
|
+
end
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
alias_method :to_options, :symbolize_keys
|
31
|
+
alias_method :to_options!, :symbolize_keys!
|
32
|
+
|
33
|
+
# Validate all keys in a hash match *valid keys, raising ArgumentError on a mismatch.
|
34
|
+
# Note that keys are NOT treated indifferently, meaning if you use strings for keys but assert symbols
|
35
|
+
# as keys, this will fail.
|
36
|
+
#
|
37
|
+
# ==== Examples
|
38
|
+
# { :name => "Rob", :years => "28" }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key(s): years"
|
39
|
+
# { :name => "Rob", :age => "28" }.assert_valid_keys("name", "age") # => raises "ArgumentError: Unknown key(s): name, age"
|
40
|
+
# { :name => "Rob", :age => "28" }.assert_valid_keys(:name, :age) # => passes, raises nothing
|
41
|
+
def assert_valid_keys(*valid_keys)
|
42
|
+
unknown_keys = keys - [valid_keys].flatten
|
43
|
+
raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
# Returns an <tt>ActiveSupport::HashWithIndifferentAccess</tt> out of its receiver:
|
48
|
+
#
|
49
|
+
# {:a => 1}.with_indifferent_access["a"] # => 1
|
50
|
+
#
|
51
|
+
def with_indifferent_access
|
52
|
+
ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default(self)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Called when object is nested under an object that receives
|
56
|
+
# #with_indifferent_access. This method will be called on the current object
|
57
|
+
# by the enclosing object and is aliased to #with_indifferent_access by
|
58
|
+
# default. Subclasses of Hash may overwrite this method to return +self+ if
|
59
|
+
# converting to an <tt>ActiveSupport::HashWithIndifferentAccess</tt> would not be
|
60
|
+
# desirable.
|
61
|
+
#
|
62
|
+
# b = {:b => 1}
|
63
|
+
# {:a => b}.with_indifferent_access["a"] # calls b.nested_under_indifferent_access
|
64
|
+
#
|
65
|
+
alias nested_under_indifferent_access with_indifferent_access
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
# This class has dubious semantics and we only have it so that
|
70
|
+
# people can write <tt>params[:key]</tt> instead of <tt>params['key']</tt>
|
71
|
+
# and they get the same value for both keys.
|
72
|
+
|
73
|
+
module ActiveSupport
|
74
|
+
class HashWithIndifferentAccess < Hash
|
75
|
+
|
76
|
+
# Always returns true, so that <tt>Array#extract_options!</tt> finds members of this class.
|
77
|
+
def extractable_options?
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
def with_indifferent_access
|
82
|
+
dup
|
83
|
+
end
|
84
|
+
|
85
|
+
def nested_under_indifferent_access
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
def initialize(constructor = {})
|
90
|
+
if constructor.is_a?(Hash)
|
91
|
+
super()
|
92
|
+
update(constructor)
|
93
|
+
else
|
94
|
+
super(constructor)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def default(key = nil)
|
99
|
+
if key.is_a?(Symbol) && include?(key = key.to_s)
|
100
|
+
self[key]
|
101
|
+
else
|
102
|
+
super
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.new_from_hash_copying_default(hash)
|
107
|
+
new(hash).tap do |new_hash|
|
108
|
+
new_hash.default = hash.default
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
|
113
|
+
alias_method :regular_update, :update unless method_defined?(:regular_update)
|
114
|
+
|
115
|
+
# Assigns a new value to the hash:
|
116
|
+
#
|
117
|
+
# hash = HashWithIndifferentAccess.new
|
118
|
+
# hash[:key] = "value"
|
119
|
+
#
|
120
|
+
def []=(key, value)
|
121
|
+
regular_writer(convert_key(key), convert_value(value))
|
122
|
+
end
|
123
|
+
|
124
|
+
alias_method :store, :[]=
|
125
|
+
|
126
|
+
# Updates the instantized hash with values from the second:
|
127
|
+
#
|
128
|
+
# hash_1 = HashWithIndifferentAccess.new
|
129
|
+
# hash_1[:key] = "value"
|
130
|
+
#
|
131
|
+
# hash_2 = HashWithIndifferentAccess.new
|
132
|
+
# hash_2[:key] = "New Value!"
|
133
|
+
#
|
134
|
+
# hash_1.update(hash_2) # => {"key"=>"New Value!"}
|
135
|
+
#
|
136
|
+
def update(other_hash)
|
137
|
+
if other_hash.is_a? HashWithIndifferentAccess
|
138
|
+
super(other_hash)
|
139
|
+
else
|
140
|
+
other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
|
141
|
+
self
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
alias_method :merge!, :update
|
146
|
+
|
147
|
+
# Checks the hash for a key matching the argument passed in:
|
148
|
+
#
|
149
|
+
# hash = HashWithIndifferentAccess.new
|
150
|
+
# hash["key"] = "value"
|
151
|
+
# hash.key? :key # => true
|
152
|
+
# hash.key? "key" # => true
|
153
|
+
#
|
154
|
+
def key?(key)
|
155
|
+
super(convert_key(key))
|
156
|
+
end
|
157
|
+
|
158
|
+
alias_method :include?, :key?
|
159
|
+
alias_method :has_key?, :key?
|
160
|
+
alias_method :member?, :key?
|
161
|
+
|
162
|
+
# Fetches the value for the specified key, same as doing hash[key]
|
163
|
+
def fetch(key, *extras)
|
164
|
+
super(convert_key(key), *extras)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Returns an array of the values at the specified indices:
|
168
|
+
#
|
169
|
+
# hash = HashWithIndifferentAccess.new
|
170
|
+
# hash[:a] = "x"
|
171
|
+
# hash[:b] = "y"
|
172
|
+
# hash.values_at("a", "b") # => ["x", "y"]
|
173
|
+
#
|
174
|
+
def values_at(*indices)
|
175
|
+
indices.collect {|key| self[convert_key(key)]}
|
176
|
+
end
|
177
|
+
|
178
|
+
# Returns an exact copy of the hash.
|
179
|
+
def dup
|
180
|
+
self.class.new(self).tap do |new_hash|
|
181
|
+
new_hash.default = default
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Merges the instantized and the specified hashes together, giving precedence to the values from the second hash.
|
186
|
+
# Does not overwrite the existing hash.
|
187
|
+
def merge(hash)
|
188
|
+
self.dup.update(hash)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
|
192
|
+
# This overloaded definition prevents returning a regular hash, if reverse_merge is called on a <tt>HashWithDifferentAccess</tt>.
|
193
|
+
def reverse_merge(other_hash)
|
194
|
+
super self.class.new_from_hash_copying_default(other_hash)
|
195
|
+
end
|
196
|
+
|
197
|
+
def reverse_merge!(other_hash)
|
198
|
+
replace(reverse_merge( other_hash ))
|
199
|
+
end
|
200
|
+
|
201
|
+
# Removes a specified key from the hash.
|
202
|
+
def delete(key)
|
203
|
+
super(convert_key(key))
|
204
|
+
end
|
205
|
+
|
206
|
+
def stringify_keys!; self end
|
207
|
+
def stringify_keys; dup end
|
208
|
+
undef :symbolize_keys!
|
209
|
+
def symbolize_keys; to_hash.symbolize_keys end
|
210
|
+
def to_options!; self end
|
211
|
+
|
212
|
+
# Convert to a Hash with String keys.
|
213
|
+
def to_hash
|
214
|
+
Hash.new(default).merge!(self)
|
215
|
+
end
|
216
|
+
|
217
|
+
protected
|
218
|
+
def convert_key(key)
|
219
|
+
key.kind_of?(Symbol) ? key.to_s : key
|
220
|
+
end
|
221
|
+
|
222
|
+
def convert_value(value)
|
223
|
+
if value.is_a? Hash
|
224
|
+
value.nested_under_indifferent_access
|
225
|
+
elsif value.is_a?(Array)
|
226
|
+
value.dup.replace(value.map { |e| convert_value(e) })
|
227
|
+
else
|
228
|
+
value
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess
|
@@ -0,0 +1,114 @@
|
|
1
|
+
class Object
|
2
|
+
# An object is blank if it's false, empty, or a whitespace string.
|
3
|
+
# For example, "", " ", +nil+, [], and {} are all blank.
|
4
|
+
#
|
5
|
+
# This simplifies:
|
6
|
+
#
|
7
|
+
# if address.nil? || address.empty?
|
8
|
+
#
|
9
|
+
# ...to:
|
10
|
+
#
|
11
|
+
# if address.blank?
|
12
|
+
def blank?
|
13
|
+
respond_to?(:empty?) ? empty? : !self
|
14
|
+
end
|
15
|
+
|
16
|
+
# An object is present if it's not <tt>blank?</tt>.
|
17
|
+
def present?
|
18
|
+
!blank?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns object if it's <tt>present?</tt> otherwise returns +nil+.
|
22
|
+
# <tt>object.presence</tt> is equivalent to <tt>object.present? ? object : nil</tt>.
|
23
|
+
#
|
24
|
+
# This is handy for any representation of objects where blank is the same
|
25
|
+
# as not present at all. For example, this simplifies a common check for
|
26
|
+
# HTTP POST/query parameters:
|
27
|
+
#
|
28
|
+
# state = params[:state] if params[:state].present?
|
29
|
+
# country = params[:country] if params[:country].present?
|
30
|
+
# region = state || country || 'US'
|
31
|
+
#
|
32
|
+
# ...becomes:
|
33
|
+
#
|
34
|
+
# region = params[:state].presence || params[:country].presence || 'US'
|
35
|
+
def presence
|
36
|
+
self if present?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class NilClass
|
41
|
+
# +nil+ is blank:
|
42
|
+
#
|
43
|
+
# nil.blank? # => true
|
44
|
+
#
|
45
|
+
def blank?
|
46
|
+
true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class FalseClass
|
51
|
+
# +false+ is blank:
|
52
|
+
#
|
53
|
+
# false.blank? # => true
|
54
|
+
#
|
55
|
+
def blank?
|
56
|
+
true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class TrueClass
|
61
|
+
# +true+ is not blank:
|
62
|
+
#
|
63
|
+
# true.blank? # => false
|
64
|
+
#
|
65
|
+
def blank?
|
66
|
+
false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Array
|
71
|
+
# An array is blank if it's empty:
|
72
|
+
#
|
73
|
+
# [].blank? # => true
|
74
|
+
# [1,2,3].blank? # => false
|
75
|
+
#
|
76
|
+
alias_method :blank?, :empty?
|
77
|
+
end
|
78
|
+
|
79
|
+
class Hash
|
80
|
+
# A hash is blank if it's empty:
|
81
|
+
#
|
82
|
+
# {}.blank? # => true
|
83
|
+
# {:key => 'value'}.blank? # => false
|
84
|
+
#
|
85
|
+
alias_method :blank?, :empty?
|
86
|
+
end
|
87
|
+
|
88
|
+
class String
|
89
|
+
# 0x3000: fullwidth whitespace
|
90
|
+
NON_WHITESPACE_REGEXP = %r![^\s#{[0x3000].pack("U")}]!
|
91
|
+
|
92
|
+
# A string is blank if it's empty or contains whitespaces only:
|
93
|
+
#
|
94
|
+
# "".blank? # => true
|
95
|
+
# " ".blank? # => true
|
96
|
+
# " ".blank? # => true
|
97
|
+
# " something here ".blank? # => false
|
98
|
+
#
|
99
|
+
def blank?
|
100
|
+
# 1.8 does not takes [:space:] properly
|
101
|
+
self !~ NON_WHITESPACE_REGEXP
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class Numeric #:nodoc:
|
106
|
+
# No number is blank:
|
107
|
+
#
|
108
|
+
# 1.blank? # => false
|
109
|
+
# 0.blank? # => false
|
110
|
+
#
|
111
|
+
def blank?
|
112
|
+
false
|
113
|
+
end
|
114
|
+
end
|
data/lib/sheet_mapper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'core_ext/hash_ext' unless Hash.method_defined?(:symbolize_keys)
|
4
|
+
require 'core_ext/object_ext' unless Object.method_defined?(:present?)
|
5
|
+
require 'google_spreadsheet'
|
6
|
+
require 'sheet_mapper/version'
|
7
|
+
require 'sheet_mapper/collection'
|
8
|
+
require 'sheet_mapper/spreadsheet'
|
9
|
+
require 'sheet_mapper/base'
|
10
|
+
|
11
|
+
module SheetMapper
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module SheetMapper
|
2
|
+
class Base
|
3
|
+
|
4
|
+
# SheetMapper::Base.new(0, ["foo", "bar"])
|
5
|
+
def initialize(pos, data=[])
|
6
|
+
@pos = pos
|
7
|
+
@data = data
|
8
|
+
@attrs = process_data
|
9
|
+
end
|
10
|
+
|
11
|
+
# columns :offset_seconds, :body, :link_url, :category
|
12
|
+
def self.columns(*names)
|
13
|
+
names.any? ? @columns = names : @columns
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the spreadsheet as a hash
|
17
|
+
def attributes
|
18
|
+
result = HashWithIndifferentAccess.new
|
19
|
+
@attrs.each do |name, val|
|
20
|
+
result[name] = self.respond_to?(name) ? self.send(name) : val
|
21
|
+
end
|
22
|
+
result
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns an attribute value
|
26
|
+
# @record[:attr_name]
|
27
|
+
def [](name)
|
28
|
+
@attrs[name]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Assign an attribute value
|
32
|
+
# @record[:attr_name]
|
33
|
+
def []=(name, val)
|
34
|
+
@attrs[name] = val
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns true if the row is a valid record
|
38
|
+
def valid_row?
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
# column_order => [:offset_seconds, :body, :link_url, :category]
|
45
|
+
def column_order
|
46
|
+
self.class.columns
|
47
|
+
end
|
48
|
+
|
49
|
+
# column_pos(:offset_seconds) => 1
|
50
|
+
# column_pos(:body) => 4
|
51
|
+
def column_pos(name)
|
52
|
+
self.column_order.index(name)
|
53
|
+
end
|
54
|
+
|
55
|
+
def log(text, newline=true)
|
56
|
+
output = newline ? method(:puts) : method(:print)
|
57
|
+
output.call(text) if LOG
|
58
|
+
end # log
|
59
|
+
|
60
|
+
# Process all columns into an attribute hash
|
61
|
+
def process_data
|
62
|
+
m = HashWithIndifferentAccess.new
|
63
|
+
column_order.each { |name| m[name.to_s] = self.attribute_value(name) }
|
64
|
+
m
|
65
|
+
end
|
66
|
+
|
67
|
+
# attribute_value(:body, 1, 1) => "Foo"
|
68
|
+
# attribute_value(:image_url, 1, 3) => nil
|
69
|
+
# attribute_value(:link_text, 2) => "Article"
|
70
|
+
# Create a method "format_<name>" to transform the column value (or pass the value directly)
|
71
|
+
# Column position defaults to matching named column in `column_order`
|
72
|
+
def attribute_value(name)
|
73
|
+
val = @data[column_pos(name)]
|
74
|
+
val = val.to_i if val && name.to_s =~ /_(id|num)/
|
75
|
+
val
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module SheetMapper
|
2
|
+
class CollectionNotFound < StandardError; end
|
3
|
+
|
4
|
+
class Collection
|
5
|
+
attr_reader :records, :worksheet
|
6
|
+
|
7
|
+
# spreadsheet is a SheetMapper::Spreadsheet
|
8
|
+
# SheetMapper::Collection.new(@sheet, @work)
|
9
|
+
def initialize(spreadsheet, worksheet)
|
10
|
+
@spreadsheet = spreadsheet
|
11
|
+
@worksheet = worksheet
|
12
|
+
@mapper = @spreadsheet.mapper
|
13
|
+
@records = process_records!
|
14
|
+
end
|
15
|
+
|
16
|
+
# Each block for every mapped record
|
17
|
+
# @collection.each { |m| ...mapped obj... }
|
18
|
+
def each(&block)
|
19
|
+
records.each(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns an array of mapped records
|
23
|
+
# @collection.rows => [<SheetMapper::Base>, ...]
|
24
|
+
def rows
|
25
|
+
@worksheet.rows
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns raw value from worksheet cell
|
29
|
+
# @collection.cell(4, 5) => "Bob"
|
30
|
+
def cell(row, col)
|
31
|
+
@worksheet[row, col]
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
# Converts all valid raw data hashes into mapped records
|
37
|
+
# process_records! => [<SheetMapper::Base>, ...]
|
38
|
+
def process_records!
|
39
|
+
records = []
|
40
|
+
@worksheet.rows.each_with_index do |record, index|
|
41
|
+
record = @mapper.new(index, record)
|
42
|
+
records << record if record.valid_row?
|
43
|
+
end
|
44
|
+
records
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module SheetMapper
|
2
|
+
class Spreadsheet
|
3
|
+
attr_reader :mapper, :session, :spreadsheet
|
4
|
+
|
5
|
+
# SheetMapper::Worksheet.new(:mapper => SomeMapper, :key => 'sheet_key', :login => 'user', :password => 'pass')
|
6
|
+
def initialize(options={})
|
7
|
+
@mapper = options[:mapper]
|
8
|
+
@session = ::GoogleSpreadsheet.login(options[:login], options[:password])
|
9
|
+
@spreadsheet = @session.spreadsheet_by_key(options[:key])
|
10
|
+
end
|
11
|
+
|
12
|
+
# Returns the first worksheet with given title
|
13
|
+
# sheet.find_collection_by_title('title') => <SheetMapper::Collection>
|
14
|
+
def find_collection_by_title(val)
|
15
|
+
val_pattern = /#{val.to_s.downcase.gsub(/\s/, '')}/
|
16
|
+
worksheet = self.spreadsheet.worksheets.find { |w| w.title.downcase.gsub(/\s/, '') =~ val_pattern }
|
17
|
+
raise CollectionNotFound, "No worksheet found '#{val}'! Please try again." unless worksheet
|
18
|
+
Collection.new(self, worksheet)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "sheet_mapper/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "sheet_mapper"
|
7
|
+
s.version = SheetMapper::VERSION
|
8
|
+
s.authors = ["Nathan Esquenazi"]
|
9
|
+
s.email = ["nesquena@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Map google spreadsheets to ruby objects}
|
12
|
+
s.description = %q{Map google spreadsheets to ruby objects.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "sheet_mapper"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency 'google-spreadsheet-ruby', '~> 0.2.1'
|
22
|
+
|
23
|
+
s.add_development_dependency 'minitest', "~> 2.11.0"
|
24
|
+
s.add_development_dependency 'rake'
|
25
|
+
s.add_development_dependency 'mocha'
|
26
|
+
s.add_development_dependency 'fakeweb'
|
27
|
+
s.add_development_dependency 'awesome_print'
|
28
|
+
end
|
data/test/base_test.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require File.expand_path('../test_config.rb', __FILE__)
|
2
|
+
|
3
|
+
class TestBase < SheetMapper::Base
|
4
|
+
columns :name, :age, :color
|
5
|
+
|
6
|
+
def age
|
7
|
+
self[:age] * 2
|
8
|
+
end
|
9
|
+
|
10
|
+
def name
|
11
|
+
self[:name].upcase
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "Base" do
|
16
|
+
setup do
|
17
|
+
@data = ["Bob", 21, "Red"]
|
18
|
+
end
|
19
|
+
|
20
|
+
should "support columns accessor" do
|
21
|
+
assert_equal [:name, :age, :color], TestBase.columns
|
22
|
+
end
|
23
|
+
|
24
|
+
context "for attributes method" do
|
25
|
+
setup do
|
26
|
+
@test = TestBase.new(1, @data)
|
27
|
+
end
|
28
|
+
|
29
|
+
should "return attribute hash" do
|
30
|
+
assert_equal "BOB", @test.name
|
31
|
+
assert_equal 42, @test.age
|
32
|
+
assert_equal "Red", @test[:color]
|
33
|
+
end
|
34
|
+
end # attributes
|
35
|
+
|
36
|
+
context "for accessing attribute" do
|
37
|
+
setup do
|
38
|
+
@test = TestBase.new(1, @data)
|
39
|
+
end
|
40
|
+
|
41
|
+
should "return value of attribute" do
|
42
|
+
assert_equal "Bob", @test[:name]
|
43
|
+
end
|
44
|
+
end # access []
|
45
|
+
|
46
|
+
context "for assigning attributes" do
|
47
|
+
setup do
|
48
|
+
@test = TestBase.new(1, @data)
|
49
|
+
@test[:age] = 45
|
50
|
+
end
|
51
|
+
|
52
|
+
should "have new value assigned" do
|
53
|
+
assert_equal 45, @test[:age]
|
54
|
+
end
|
55
|
+
end # assign []=
|
56
|
+
end # Base
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require File.expand_path('../test_config.rb', __FILE__)
|
2
|
+
|
3
|
+
class TestMapper
|
4
|
+
attr_reader :index, :record
|
5
|
+
|
6
|
+
def initialize(index, record)
|
7
|
+
@index = index
|
8
|
+
@record = record
|
9
|
+
end
|
10
|
+
|
11
|
+
def tuple; [name, age]; end
|
12
|
+
def name; record[:name]; end
|
13
|
+
def age; record[:age]; end
|
14
|
+
def valid_row?; true; end
|
15
|
+
end # TestMapper
|
16
|
+
|
17
|
+
describe "Collection" do
|
18
|
+
setup do
|
19
|
+
@worksheet = stub(:rows => [{ :name => "Bob", :age => 21 }, { :name => "Susan", :age => 34 }, { :name => "Joey", :age => 67 }])
|
20
|
+
@spreadsheet = stub(:mapper => TestMapper)
|
21
|
+
end
|
22
|
+
|
23
|
+
context "for worksheet accessor" do
|
24
|
+
setup do
|
25
|
+
@collection = SheetMapper::Collection.new(@spreadsheet, @worksheet)
|
26
|
+
end
|
27
|
+
|
28
|
+
should "return worksheet from accessor" do
|
29
|
+
assert_equal @worksheet, @collection.worksheet
|
30
|
+
end
|
31
|
+
end # worksheet
|
32
|
+
|
33
|
+
context "for each method" do
|
34
|
+
setup do
|
35
|
+
@collection = SheetMapper::Collection.new(@spreadsheet, @worksheet)
|
36
|
+
end
|
37
|
+
|
38
|
+
should "support iterating each mapped row" do
|
39
|
+
rows = []
|
40
|
+
@collection.each { |c| rows << c }
|
41
|
+
assert_equal 3, rows.size
|
42
|
+
assert_equal ["Bob", 21], rows[0].tuple
|
43
|
+
assert_equal ["Susan", 34], rows[1].tuple
|
44
|
+
assert_equal ["Joey", 67], rows[2].tuple
|
45
|
+
end
|
46
|
+
end # each
|
47
|
+
|
48
|
+
context "for rows method" do
|
49
|
+
setup do
|
50
|
+
@collection = SheetMapper::Collection.new(@spreadsheet, @worksheet)
|
51
|
+
end
|
52
|
+
|
53
|
+
should "return raw data hashes" do
|
54
|
+
rows = @collection.rows
|
55
|
+
assert_equal ["Bob", 21], [rows[0][:name], rows[0][:age]]
|
56
|
+
assert_equal ["Susan", 34], [rows[1][:name], rows[1][:age]]
|
57
|
+
assert_equal ["Joey", 67], [rows[2][:name], rows[2][:age]]
|
58
|
+
end
|
59
|
+
end # rows
|
60
|
+
|
61
|
+
context "for cell method" do
|
62
|
+
setup do
|
63
|
+
@worksheet.expects(:[]).with(4,5).returns("foo")
|
64
|
+
@collection = SheetMapper::Collection.new(@spreadsheet, @worksheet)
|
65
|
+
end
|
66
|
+
|
67
|
+
should "return raw data in worksheet" do
|
68
|
+
assert_equal "foo", @collection.cell(4,5)
|
69
|
+
end
|
70
|
+
end # cell
|
71
|
+
|
72
|
+
context "for records method" do
|
73
|
+
setup do
|
74
|
+
@collection = SheetMapper::Collection.new(@spreadsheet, @worksheet)
|
75
|
+
end
|
76
|
+
|
77
|
+
should "support iterating each mapped row" do
|
78
|
+
rows = @collection.records
|
79
|
+
assert_equal 3, rows.size
|
80
|
+
assert_equal ["Bob", 21], rows[0].tuple
|
81
|
+
assert_equal ["Susan", 34], rows[1].tuple
|
82
|
+
assert_equal ["Joey", 67], rows[2].tuple
|
83
|
+
end
|
84
|
+
end # records
|
85
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.expand_path('../test_config.rb', __FILE__)
|
2
|
+
|
3
|
+
describe "Spreadsheet" do
|
4
|
+
setup do
|
5
|
+
@sheet_stub = stub(:sheet)
|
6
|
+
@session_stub = stub(:session)
|
7
|
+
@session_stub.expects(:spreadsheet_by_key).with('foo').returns(@sheet_stub)
|
8
|
+
::GoogleSpreadsheet.expects(:login).with('login', 'pass').returns(@session_stub)
|
9
|
+
end
|
10
|
+
|
11
|
+
context "for initialize" do
|
12
|
+
setup do
|
13
|
+
@sheet = SheetMapper::Spreadsheet.new(:mapper => Object, :key => 'foo', :login => 'login', :password => 'pass')
|
14
|
+
end
|
15
|
+
|
16
|
+
should "return spreadsheet class" do
|
17
|
+
assert_kind_of SheetMapper::Spreadsheet, @sheet
|
18
|
+
end
|
19
|
+
|
20
|
+
should "have access to readers" do
|
21
|
+
assert_equal Object, @sheet.mapper
|
22
|
+
assert_equal @session_stub, @sheet.session
|
23
|
+
assert_equal @sheet_stub, @sheet.spreadsheet
|
24
|
+
end
|
25
|
+
end # initialize
|
26
|
+
|
27
|
+
context "for find_collection_by_title method" do
|
28
|
+
setup do
|
29
|
+
@sheet = SheetMapper::Spreadsheet.new(:mapper => Object, :key => 'foo', :login => 'login', :password => 'pass')
|
30
|
+
@work_stub = stub(:worksheet)
|
31
|
+
@work_stub.expects(:title).returns("FOO")
|
32
|
+
@work_stub.expects(:rows).returns([])
|
33
|
+
@sheet_stub.expects(:worksheets).returns([@work_stub])
|
34
|
+
@collection = @sheet.find_collection_by_title("foo")
|
35
|
+
end
|
36
|
+
|
37
|
+
should "return the expected collection" do
|
38
|
+
assert_kind_of SheetMapper::Collection, @collection
|
39
|
+
assert_equal @work_stub, @collection.worksheet
|
40
|
+
end
|
41
|
+
end # find_collection_by_title
|
42
|
+
end # Spreadsheet
|
data/test/test_config.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'mocha'
|
3
|
+
require 'fakeweb'
|
4
|
+
require 'ap'
|
5
|
+
require File.expand_path('../../lib/sheet_mapper', __FILE__)
|
6
|
+
|
7
|
+
Dir[File.expand_path("../test_helpers/*.rb", __FILE__)].each { |f| require f }
|
8
|
+
FakeWeb.allow_net_connect = false
|
9
|
+
|
10
|
+
class MiniTest::Unit::TestCase
|
11
|
+
include Mocha::API
|
12
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
gem 'minitest'
|
2
|
+
require 'minitest/spec'
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'mocha' # Load mocha after minitest
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'ruby-debug'
|
8
|
+
rescue LoadError; end
|
9
|
+
|
10
|
+
class MiniTest::Spec
|
11
|
+
class << self
|
12
|
+
alias :setup :before unless defined?(Rails)
|
13
|
+
alias :teardown :after unless defined?(Rails)
|
14
|
+
alias :should :it
|
15
|
+
alias :context :describe
|
16
|
+
def should_eventually(desc)
|
17
|
+
it("should eventually #{desc}") { skip("Should eventually #{desc}") }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
alias :assert_no_match :refute_match
|
21
|
+
alias :assert_not_nil :refute_nil
|
22
|
+
alias :assert_not_equal :refute_equal
|
23
|
+
|
24
|
+
# assert_same_elements([:a, :b, :c], [:c, :a, :b]) => passes
|
25
|
+
def assert_same_elements(a1, a2, msg = nil)
|
26
|
+
[:select, :inject, :size].each do |m|
|
27
|
+
[a1, a2].each {|a| assert_respond_to(a, m, "Are you sure that #{a.inspect} is an array? It doesn't respond to #{m}.") }
|
28
|
+
end
|
29
|
+
|
30
|
+
assert a1h = a1.inject({}) { |h,e| h[e] ||= a1.select { |i| i == e }.size; h }
|
31
|
+
assert a2h = a2.inject({}) { |h,e| h[e] ||= a2.select { |i| i == e }.size; h }
|
32
|
+
|
33
|
+
assert_equal(a1h, a2h, msg)
|
34
|
+
end
|
35
|
+
|
36
|
+
# assert_contains(['a', '1'], /\d/) => passes
|
37
|
+
# assert_contains(['a', '1'], 'a') => passes
|
38
|
+
# assert_contains(['a', '1'], /not there/) => fails
|
39
|
+
def assert_contains(collection, x, extra_msg = "")
|
40
|
+
collection = [collection] unless collection.is_a?(Array)
|
41
|
+
msg = "#{x.inspect} not found in #{collection.to_a.inspect} #{extra_msg}"
|
42
|
+
case x
|
43
|
+
when Regexp
|
44
|
+
assert(collection.detect { |e| e =~ x }, msg)
|
45
|
+
else
|
46
|
+
assert(collection.include?(x), msg)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Asserts that the given collection does not contain item x. If x is a regular expression, ensure that
|
51
|
+
# none of the elements from the collection match x.
|
52
|
+
def assert_does_not_contain(collection, x, extra_msg = "")
|
53
|
+
collection = [collection] unless collection.is_a?(Array)
|
54
|
+
msg = "#{x.inspect} found in #{collection.to_a.inspect} " + extra_msg
|
55
|
+
case x
|
56
|
+
when Regexp
|
57
|
+
assert(!collection.detect { |e| e =~ x }, msg)
|
58
|
+
else
|
59
|
+
assert(!collection.include?(x), msg)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end # MiniTest::Spec
|
63
|
+
|
64
|
+
class ColoredIO
|
65
|
+
def initialize(io)
|
66
|
+
@io = io
|
67
|
+
end
|
68
|
+
|
69
|
+
def print(o)
|
70
|
+
case o
|
71
|
+
when "." then @io.send(:print, o.green)
|
72
|
+
when "E" then @io.send(:print, o.yellow)
|
73
|
+
when "F" then @io.send(:print, o.red)
|
74
|
+
when "S" then @io.send(:print, o.magenta)
|
75
|
+
else @io.send(:print, o)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def puts(*o)
|
80
|
+
super
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
MiniTest::Unit.output = ColoredIO.new(MiniTest::Unit.output)
|
metadata
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sheet_mapper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Nathan Esquenazi
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-05-15 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: google-spreadsheet-ruby
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 21
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
- 2
|
33
|
+
- 1
|
34
|
+
version: 0.2.1
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: minitest
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 35
|
46
|
+
segments:
|
47
|
+
- 2
|
48
|
+
- 11
|
49
|
+
- 0
|
50
|
+
version: 2.11.0
|
51
|
+
type: :development
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: rake
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 3
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
version: "0"
|
65
|
+
type: :development
|
66
|
+
version_requirements: *id003
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: mocha
|
69
|
+
prerelease: false
|
70
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
hash: 3
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
version: "0"
|
79
|
+
type: :development
|
80
|
+
version_requirements: *id004
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: fakeweb
|
83
|
+
prerelease: false
|
84
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
hash: 3
|
90
|
+
segments:
|
91
|
+
- 0
|
92
|
+
version: "0"
|
93
|
+
type: :development
|
94
|
+
version_requirements: *id005
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: awesome_print
|
97
|
+
prerelease: false
|
98
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
hash: 3
|
104
|
+
segments:
|
105
|
+
- 0
|
106
|
+
version: "0"
|
107
|
+
type: :development
|
108
|
+
version_requirements: *id006
|
109
|
+
description: Map google spreadsheets to ruby objects.
|
110
|
+
email:
|
111
|
+
- nesquena@gmail.com
|
112
|
+
executables: []
|
113
|
+
|
114
|
+
extensions: []
|
115
|
+
|
116
|
+
extra_rdoc_files: []
|
117
|
+
|
118
|
+
files:
|
119
|
+
- .gitignore
|
120
|
+
- Gemfile
|
121
|
+
- README.md
|
122
|
+
- Rakefile
|
123
|
+
- example.rb
|
124
|
+
- example2.rb
|
125
|
+
- lib/core_ext/hash_ext.rb
|
126
|
+
- lib/core_ext/object_ext.rb
|
127
|
+
- lib/sheet_mapper.rb
|
128
|
+
- lib/sheet_mapper/base.rb
|
129
|
+
- lib/sheet_mapper/collection.rb
|
130
|
+
- lib/sheet_mapper/spreadsheet.rb
|
131
|
+
- lib/sheet_mapper/version.rb
|
132
|
+
- sheet_mapper.gemspec
|
133
|
+
- test/base_test.rb
|
134
|
+
- test/collection_test.rb
|
135
|
+
- test/spreadsheet_test.rb
|
136
|
+
- test/test_config.rb
|
137
|
+
- test/test_helpers/mini_shoulda.rb
|
138
|
+
has_rdoc: true
|
139
|
+
homepage: ""
|
140
|
+
licenses: []
|
141
|
+
|
142
|
+
post_install_message:
|
143
|
+
rdoc_options: []
|
144
|
+
|
145
|
+
require_paths:
|
146
|
+
- lib
|
147
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
148
|
+
none: false
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
hash: 3
|
153
|
+
segments:
|
154
|
+
- 0
|
155
|
+
version: "0"
|
156
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
157
|
+
none: false
|
158
|
+
requirements:
|
159
|
+
- - ">="
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
hash: 3
|
162
|
+
segments:
|
163
|
+
- 0
|
164
|
+
version: "0"
|
165
|
+
requirements: []
|
166
|
+
|
167
|
+
rubyforge_project: sheet_mapper
|
168
|
+
rubygems_version: 1.6.2
|
169
|
+
signing_key:
|
170
|
+
specification_version: 3
|
171
|
+
summary: Map google spreadsheets to ruby objects
|
172
|
+
test_files:
|
173
|
+
- test/base_test.rb
|
174
|
+
- test/collection_test.rb
|
175
|
+
- test/spreadsheet_test.rb
|
176
|
+
- test/test_config.rb
|
177
|
+
- test/test_helpers/mini_shoulda.rb
|