sheet_mapper 0.0.1
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/.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
|