Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Initial commit

  • Loading branch information...
commit 49355262948abcf29f47e60ce5f9e1fbd3b6c5a5 0 parents
Nathan Esquenazi authored
4 .gitignore
... ... @@ -0,0 +1,4 @@
  1 +*.gem
  2 +.bundle
  3 +Gemfile.lock
  4 +pkg/*
4 Gemfile
... ... @@ -0,0 +1,4 @@
  1 +source "http://rubygems.org"
  2 +
  3 +# Specify your gem's dependencies in sheet_mapper.gemspec
  4 +gemspec
28 README.md
Source Rendered
... ... @@ -0,0 +1,28 @@
  1 +# SheetMapper
  2 +
  3 +Define a mapper:
  4 +
  5 +```ruby
  6 +class BubbleMapper < SheetMapper::Base
  7 + columns :offset_seconds, :is_notable, :category, :body, :image_url, :link_text, :link_url
  8 +
  9 + def valid_row?
  10 + self[:body].present? && @pos > 7
  11 + end
  12 +
  13 + # Convert is_notable to boolean
  14 + def is_notable
  15 + self[:is_notable].to_s.match(/true/i).present?
  16 + end
  17 +end
  18 +```
  19 +
  20 +Use a mapper:
  21 +
  22 +```ruby
  23 +sheet = SheetMapper::Worksheet.new(:mapper => BubbleMapper, :key => 'sheet_key', :login => 'user', :password => 'pass')
  24 +collection = sheet.find_collection_by_title('title')
  25 +bubbles = collection.each do |bubble|
  26 + p bubble.to_hash
  27 +end
  28 +```
1  Rakefile
... ... @@ -0,0 +1 @@
  1 +require 'bundler/gem_tasks'
191 lib/core_ext/hash_ext.rb
... ... @@ -0,0 +1,191 @@
  1 +require 'active_support/core_ext/hash/keys'
  2 +
  3 +# This class has dubious semantics and we only have it so that
  4 +# people can write <tt>params[:key]</tt> instead of <tt>params['key']</tt>
  5 +# and they get the same value for both keys.
  6 +
  7 +module ActiveSupport
  8 + class HashWithIndifferentAccess < Hash
  9 +
  10 + # Always returns true, so that <tt>Array#extract_options!</tt> finds members of this class.
  11 + def extractable_options?
  12 + true
  13 + end
  14 +
  15 + def with_indifferent_access
  16 + dup
  17 + end
  18 +
  19 + def nested_under_indifferent_access
  20 + self
  21 + end
  22 +
  23 + def initialize(constructor = {})
  24 + if constructor.is_a?(Hash)
  25 + super()
  26 + update(constructor)
  27 + else
  28 + super(constructor)
  29 + end
  30 + end
  31 +
  32 + def default(key = nil)
  33 + if key.is_a?(Symbol) && include?(key = key.to_s)
  34 + self[key]
  35 + else
  36 + super
  37 + end
  38 + end
  39 +
  40 + def self.new_from_hash_copying_default(hash)
  41 + new(hash).tap do |new_hash|
  42 + new_hash.default = hash.default
  43 + end
  44 + end
  45 +
  46 + alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
  47 + alias_method :regular_update, :update unless method_defined?(:regular_update)
  48 +
  49 + # Assigns a new value to the hash:
  50 + #
  51 + # hash = HashWithIndifferentAccess.new
  52 + # hash[:key] = "value"
  53 + #
  54 + def []=(key, value)
  55 + regular_writer(convert_key(key), convert_value(value))
  56 + end
  57 +
  58 + alias_method :store, :[]=
  59 +
  60 + # Updates the instantized hash with values from the second:
  61 + #
  62 + # hash_1 = HashWithIndifferentAccess.new
  63 + # hash_1[:key] = "value"
  64 + #
  65 + # hash_2 = HashWithIndifferentAccess.new
  66 + # hash_2[:key] = "New Value!"
  67 + #
  68 + # hash_1.update(hash_2) # => {"key"=>"New Value!"}
  69 + #
  70 + def update(other_hash)
  71 + if other_hash.is_a? HashWithIndifferentAccess
  72 + super(other_hash)
  73 + else
  74 + other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
  75 + self
  76 + end
  77 + end
  78 +
  79 + alias_method :merge!, :update
  80 +
  81 + # Checks the hash for a key matching the argument passed in:
  82 + #
  83 + # hash = HashWithIndifferentAccess.new
  84 + # hash["key"] = "value"
  85 + # hash.key? :key # => true
  86 + # hash.key? "key" # => true
  87 + #
  88 + def key?(key)
  89 + super(convert_key(key))
  90 + end
  91 +
  92 + alias_method :include?, :key?
  93 + alias_method :has_key?, :key?
  94 + alias_method :member?, :key?
  95 +
  96 + # Fetches the value for the specified key, same as doing hash[key]
  97 + def fetch(key, *extras)
  98 + super(convert_key(key), *extras)
  99 + end
  100 +
  101 + # Returns an array of the values at the specified indices:
  102 + #
  103 + # hash = HashWithIndifferentAccess.new
  104 + # hash[:a] = "x"
  105 + # hash[:b] = "y"
  106 + # hash.values_at("a", "b") # => ["x", "y"]
  107 + #
  108 + def values_at(*indices)
  109 + indices.collect {|key| self[convert_key(key)]}
  110 + end
  111 +
  112 + # Returns an exact copy of the hash.
  113 + def dup
  114 + self.class.new(self).tap do |new_hash|
  115 + new_hash.default = default
  116 + end
  117 + end
  118 +
  119 + # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash.
  120 + # Does not overwrite the existing hash.
  121 + def merge(hash)
  122 + self.dup.update(hash)
  123 + end
  124 +
  125 + # Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
  126 + # This overloaded definition prevents returning a regular hash, if reverse_merge is called on a <tt>HashWithDifferentAccess</tt>.
  127 + def reverse_merge(other_hash)
  128 + super self.class.new_from_hash_copying_default(other_hash)
  129 + end
  130 +
  131 + def reverse_merge!(other_hash)
  132 + replace(reverse_merge( other_hash ))
  133 + end
  134 +
  135 + # Removes a specified key from the hash.
  136 + def delete(key)
  137 + super(convert_key(key))
  138 + end
  139 +
  140 + def stringify_keys!; self end
  141 + def stringify_keys; dup end
  142 + undef :symbolize_keys!
  143 + def symbolize_keys; to_hash.symbolize_keys end
  144 + def to_options!; self end
  145 +
  146 + # Convert to a Hash with String keys.
  147 + def to_hash
  148 + Hash.new(default).merge!(self)
  149 + end
  150 +
  151 + protected
  152 + def convert_key(key)
  153 + key.kind_of?(Symbol) ? key.to_s : key
  154 + end
  155 +
  156 + def convert_value(value)
  157 + if value.is_a? Hash
  158 + value.nested_under_indifferent_access
  159 + elsif value.is_a?(Array)
  160 + value.dup.replace(value.map { |e| convert_value(e) })
  161 + else
  162 + value
  163 + end
  164 + end
  165 + end
  166 +end
  167 +
  168 +HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess
  169 +
  170 +class Hash
  171 +
  172 + # Returns an <tt>ActiveSupport::HashWithIndifferentAccess</tt> out of its receiver:
  173 + #
  174 + # {:a => 1}.with_indifferent_access["a"] # => 1
  175 + #
  176 + def with_indifferent_access
  177 + ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default(self)
  178 + end
  179 +
  180 + # Called when object is nested under an object that receives
  181 + # #with_indifferent_access. This method will be called on the current object
  182 + # by the enclosing object and is aliased to #with_indifferent_access by
  183 + # default. Subclasses of Hash may overwrite this method to return +self+ if
  184 + # converting to an <tt>ActiveSupport::HashWithIndifferentAccess</tt> would not be
  185 + # desirable.
  186 + #
  187 + # b = {:b => 1}
  188 + # {:a => b}.with_indifferent_access["a"] # calls b.nested_under_indifferent_access
  189 + #
  190 + alias nested_under_indifferent_access with_indifferent_access
  191 +end
115 lib/core_ext/object_ext.rb
... ... @@ -0,0 +1,115 @@
  1 +
  2 +class Object
  3 + # An object is blank if it's false, empty, or a whitespace string.
  4 + # For example, "", " ", +nil+, [], and {} are all blank.
  5 + #
  6 + # This simplifies:
  7 + #
  8 + # if address.nil? || address.empty?
  9 + #
  10 + # ...to:
  11 + #
  12 + # if address.blank?
  13 + def blank?
  14 + respond_to?(:empty?) ? empty? : !self
  15 + end
  16 +
  17 + # An object is present if it's not <tt>blank?</tt>.
  18 + def present?
  19 + !blank?
  20 + end
  21 +
  22 + # Returns object if it's <tt>present?</tt> otherwise returns +nil+.
  23 + # <tt>object.presence</tt> is equivalent to <tt>object.present? ? object : nil</tt>.
  24 + #
  25 + # This is handy for any representation of objects where blank is the same
  26 + # as not present at all. For example, this simplifies a common check for
  27 + # HTTP POST/query parameters:
  28 + #
  29 + # state = params[:state] if params[:state].present?
  30 + # country = params[:country] if params[:country].present?
  31 + # region = state || country || 'US'
  32 + #
  33 + # ...becomes:
  34 + #
  35 + # region = params[:state].presence || params[:country].presence || 'US'
  36 + def presence
  37 + self if present?
  38 + end
  39 +end
  40 +
  41 +class NilClass
  42 + # +nil+ is blank:
  43 + #
  44 + # nil.blank? # => true
  45 + #
  46 + def blank?
  47 + true
  48 + end
  49 +end
  50 +
  51 +class FalseClass
  52 + # +false+ is blank:
  53 + #
  54 + # false.blank? # => true
  55 + #
  56 + def blank?
  57 + true
  58 + end
  59 +end
  60 +
  61 +class TrueClass
  62 + # +true+ is not blank:
  63 + #
  64 + # true.blank? # => false
  65 + #
  66 + def blank?
  67 + false
  68 + end
  69 +end
  70 +
  71 +class Array
  72 + # An array is blank if it's empty:
  73 + #
  74 + # [].blank? # => true
  75 + # [1,2,3].blank? # => false
  76 + #
  77 + alias_method :blank?, :empty?
  78 +end
  79 +
  80 +class Hash
  81 + # A hash is blank if it's empty:
  82 + #
  83 + # {}.blank? # => true
  84 + # {:key => 'value'}.blank? # => false
  85 + #
  86 + alias_method :blank?, :empty?
  87 +end
  88 +
  89 +class String
  90 + # 0x3000: fullwidth whitespace
  91 + NON_WHITESPACE_REGEXP = %r![^\s#{[0x3000].pack("U")}]!
  92 +
  93 + # A string is blank if it's empty or contains whitespaces only:
  94 + #
  95 + # "".blank? # => true
  96 + # " ".blank? # => true
  97 + # " ".blank? # => true
  98 + # " something here ".blank? # => false
  99 + #
  100 + def blank?
  101 + # 1.8 does not takes [:space:] properly
  102 + self !~ NON_WHITESPACE_REGEXP
  103 + end
  104 +end
  105 +
  106 +class Numeric #:nodoc:
  107 + # No number is blank:
  108 + #
  109 + # 1.blank? # => false
  110 + # 0.blank? # => false
  111 + #
  112 + def blank?
  113 + false
  114 + end
  115 +end
10 lib/sheet_mapper.rb
... ... @@ -0,0 +1,10 @@
  1 +require File.expand_path("../sheet_mapper/version", __FILE__)
  2 +require File.expand_path("../core_ext/hash_ext", __FILE__) unless defined?(HashWithIndifferentAccess)
  3 +require File.expand_path("../core_ext/object_ext", __FILE__) unless String.method_defined?(:blank?)
  4 +require File.expand_path("../sheet_mapper/collection", __FILE__)
  5 +require File.expand_path("../sheet_mapper/spreadsheet", __FILE__)
  6 +require File.expand_path("../sheet_mapper/base", __FILE__)
  7 +
  8 +module SheetMapper
  9 + # Your code goes here...
  10 +end
78 lib/sheet_mapper/base.rb
... ... @@ -0,0 +1,78 @@
  1 +module SheetMapper
  2 + class Base
  3 + # SheetMapper::Base.new(0, ["foo", "bar"])
  4 + def initialize(pos, data=[])
  5 + @pos = pos
  6 + @data = data
  7 + @attrs = process_data
  8 + end
  9 +
  10 + # columns :offset_seconds, :body, :link_url, :category
  11 + def self.columns(*names)
  12 + names.any? ? @columns = names : @columns
  13 + end
  14 +
  15 + # Returns the spreadsheet as a hash
  16 + def attributes
  17 + result = HashWithIndifferentAccess.new
  18 + @attrs.each do |name, val|
  19 + result[name] = self.respond_to?(name) ? self.send(name) : val
  20 + end
  21 + result
  22 + end
  23 +
  24 + # Returns an attribute value
  25 + # @record[:attr_name]
  26 + def [](name)
  27 + @attrs[name]
  28 + end
  29 +
  30 + # Assign an attribute value
  31 + # @record[:attr_name]
  32 + def []=(name, val)
  33 + @attrs[name] = val
  34 + end
  35 +
  36 + # Returns true if the row is a valid record
  37 + def valid_row?
  38 + true
  39 + end
  40 +
  41 + protected
  42 +
  43 + # column_order => [:offset_seconds, :body, :link_url, :category]
  44 + def column_order
  45 + self.class.columns
  46 + end
  47 +
  48 + # column_pos(:offset_seconds) => 1
  49 + # column_pos(:body) => 4
  50 + def column_pos(name)
  51 + self.column_order.index(name)
  52 + end
  53 +
  54 + def log(text, newline=true)
  55 + output = newline ? method(:puts) : method(:print)
  56 + output.call(text) if LOG
  57 + end # log
  58 +
  59 + # Process all columns into an attribute hash
  60 + def process_data
  61 + m = HashWithIndifferentAccess.new
  62 + column_order.each { |name| m[name.to_s] = self.attribute_value(name) }
  63 + m
  64 + end
  65 +
  66 + # attribute_value(:body, 1, 1) => "Foo"
  67 + # attribute_value(:image_url, 1, 3) => nil
  68 + # attribute_value(:link_text, 2) => "Article"
  69 + # Create a method "format_<name>" to transform the column value (or pass the value directly)
  70 + # Column position defaults to matching named column in `column_order`
  71 + def attribute_value(name)
  72 + val = @data[column_pos(name)]
  73 + val = val.to_i if val && name.to_s =~ /_(id|num)/
  74 + val
  75 + end
  76 +
  77 + end
  78 +end
36 lib/sheet_mapper/collection.rb
... ... @@ -0,0 +1,36 @@
  1 +module SheetMapper
  2 + class Collection
  3 + attr_reader :records
  4 +
  5 + def initialize(spreadsheet, worksheet)
  6 + @spreadsheet = spreadsheet
  7 + @worksheet = worksheet
  8 + @mapper = @spreadsheet.mapper
  9 + @records = process_records!
  10 + end
  11 +
  12 + # @collection.each { |m| ...mapped obj... }
  13 + def each(&block)
  14 + records.each(&block)
  15 + end
  16 +
  17 + def rows
  18 + @worksheet.rows
  19 + end
  20 +
  21 + def cell(row, col)
  22 + @worksheet[row, col]
  23 + end
  24 +
  25 + protected
  26 +
  27 + def process_records!
  28 + records = []
  29 + @worksheet.rows.each_with_index do |record, index|
  30 + record = @mapper.new(index, record)
  31 + records << record if record.valid_row?
  32 + end
  33 + records
  34 + end
  35 + end
  36 +end
19 lib/sheet_mapper/spreadsheet.rb
... ... @@ -0,0 +1,19 @@
  1 +module SheetMapper
  2 + class Spreadsheet
  3 + attr_reader :mapper, :session, :spreadsheet
  4 +
  5 + # SheetMapper::Worksheet.new(:mapper => BubbleMapper, :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 + # sheet.find_collection_by_title('title')
  13 + def find_collection_by_title(val)
  14 + val_pattern = /#{val.to_s.downcase.gsub(/\s/, '')}/
  15 + worksheet = self.spreadsheet.worksheets.find { |w| w.title.downcase.gsub(/\s/, '') =~ val_pattern }
  16 + Collection.new(self, worksheet)
  17 + end
  18 + end
  19 +end
3  lib/sheet_mapper/version.rb
... ... @@ -0,0 +1,3 @@
  1 +module SheetMapper
  2 + VERSION = "0.0.1"
  3 +end
20 sheet_mapper.gemspec
... ... @@ -0,0 +1,20 @@
  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 +end

0 comments on commit 4935526

Please sign in to comment.
Something went wrong with that request. Please try again.