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.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in sheet_mapper.gemspec
4
+ gemspec
@@ -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.
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new(:test) do |test|
5
+ test.libs << 'lib' << 'test'
6
+ test.pattern = 'test/*_test.rb'
7
+ test.ruby_opts = ['-rubygems']
8
+ end
@@ -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
+
@@ -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
@@ -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,3 @@
1
+ module SheetMapper
2
+ VERSION = "0.0.1"
3
+ 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
@@ -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
@@ -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