sheet_mapper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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