mad_cart 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ vendor
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ .ruby-version
20
+ .ruby-gemset
21
+ .DS_Store
22
+ *.swp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mad_cart.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 MadMimi
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # MadCart
2
+
3
+ Provides a unified API to various CRMs and online stores.
4
+ Simple configuration allows you to specify the properties you're interested in and what they're called.
5
+ A flexible DSL allows easy CRM and store integration.
6
+
7
+ Currently supports the following stores:
8
+ **-Etsy**
9
+ **-Bigcommerce**
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'mad_cart'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install mad_cart
24
+
25
+ ## Usage
26
+
27
+ #### Credentials
28
+
29
+ Store/CRM credentials can be configured:
30
+
31
+ ```ruby
32
+ MadCart.configure do |config|
33
+ config.add_store :etsy, api_key: 'my-api-key', store_url: 'http://path.to/store'
34
+ end
35
+
36
+ store = MadCart::Store::Etsy.new
37
+ ```
38
+ ...or passed to the store initializer:
39
+
40
+ ```ruby
41
+ store = MadCart::Store::Etsy.new(api_key: 'my-api-key', store_url: 'http://path.to/store')
42
+ ```
43
+
44
+ ### Products
45
+
46
+ ```ruby
47
+ store = MadCart::Store::Etsy.new
48
+
49
+ store.products
50
+ #=> an array of MadCart::Model::Product objects
51
+ ```
52
+
53
+ ### Customers
54
+ ```ruby
55
+ store = MadCart::Store::BigCommerce.new
56
+
57
+ store.customers
58
+ # => returns an array of MadCart::Model::Customer objects
59
+ ```
60
+
61
+ ### Attributes
62
+
63
+ Each model object has a property called attributes, which returns a hash. By default the following properties are returned:
64
+
65
+ **Customers:** *first_name*, *last_name*, and *email*
66
+ **Products:** *name*, *description*, and *image_url*
67
+
68
+ #### Additional Attributes
69
+
70
+ Any additional attributes returned by the CRM or store API will be stored in the *additional_attributes* property of the object.
71
+ MadCart allows you to include any of these additional attributes in the #attributes property of the model objects:
72
+
73
+ ```ruby
74
+ store = MadCart::Store::Etsy.new
75
+
76
+ store.products.first.attributes
77
+ #=> {name: 'product name', description: 'product description', image_url 'http://path.to/image'}
78
+
79
+ MadCart.configure do |config|
80
+ config.include_attributes products: [:external_id, :url]
81
+ end
82
+
83
+ store.products.first.attributes
84
+ #=> {name: 'product name', description: 'product description', image_url 'http://path.to/image', external_id: 42, url: 'http://path.to/store/products/42'}
85
+ ```
86
+
87
+ #### Attribute Names
88
+
89
+ MadCart allows you to change the names of these attributes to match your existing field names:
90
+
91
+ ```ruby
92
+ MadCart.configure do |config|
93
+ config.attribute_map :products, {"name" => "title"}
94
+ end
95
+
96
+ store = MadCart::Store::Etsy.new
97
+
98
+ store.products.first.attributes
99
+ #=> {title: 'product name', description: 'product description', image_url 'http://path.to/image'}
100
+ ```
101
+
102
+ This, in combination with declaring additional attributes, allows for very thin integration points without sacrificing customizability:
103
+
104
+ ```ruby
105
+ store = MadCart::Store::Etsy.new
106
+ store.products.each{|p| MyProduct.create!(p.attributes) }
107
+ ```
108
+
109
+ ## Contributing
110
+
111
+ See the [Contributor's Guide](https://github.com/madmimi/mad_cart/wiki/Contributor's-Guide) for info on the store integration API.
112
+
113
+ 1. Fork it
114
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
115
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
116
+ 4. Push to the branch (`git push origin my-new-feature`)
117
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require 'yaml'
3
+ load 'vcr/tasks/vcr.rake'
4
+
5
+ require 'rspec/core/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ task :default => :spec
@@ -0,0 +1,35 @@
1
+ module MadCart
2
+ module AttributeMapper
3
+
4
+ # def attributes
5
+ # Hash[initial_attributes.map {|k, v| [(mapping_hash[k] || mapping_hash[k.to_sym] || k).to_s, v] }]
6
+ # end
7
+
8
+ def self.included(base)
9
+ base.extend ClassMethods
10
+ end
11
+
12
+ def attributes
13
+ Hash[self.class.exposed_attributes.map{|a| [a.to_s, self.send(a)]}]
14
+ end
15
+
16
+ module ClassMethods
17
+ def map_attribute_name(name)
18
+ mapping_hash[name.to_sym] || mapping_hash[name.to_s] || name
19
+ end
20
+
21
+ def mapping_hash
22
+ MadCart.config.attribute_maps[self.to_s.demodulize.underscore.pluralize] || {}
23
+ end
24
+
25
+ def mapped_attributes
26
+ mapping_hash.values
27
+ end
28
+
29
+ def unmapped_attributes
30
+ mapping_hash.keys
31
+ end
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,59 @@
1
+ require 'ostruct'
2
+
3
+ module MadCart
4
+ class << self
5
+ def configure
6
+ raise(ArgumentError, "MadCart.configure requires a block argument.") unless block_given?
7
+ yield(MadCart::Configuration.instance)
8
+ end
9
+
10
+ def config
11
+ raise(ArgumentError, "MadCart.config does not support blocks. Use MadCart.configure to set config values.") if block_given?
12
+ return MadCart::Configuration.instance.data
13
+ end
14
+ end
15
+
16
+ class Configuration
17
+ include Singleton
18
+
19
+ def add_store(store_name, args={})
20
+ setup_data
21
+
22
+ @data[:stores] << store_name
23
+ @data[store_name.to_s] = args
24
+ end
25
+
26
+ def attribute_map(data_model, attributes)
27
+ setup_data
28
+
29
+ @data[:attribute_maps][data_model.to_s] = attributes
30
+ end
31
+
32
+ def include_attributes(args={})
33
+ setup_data
34
+
35
+ @data[:included_attributes].merge!(args)
36
+ end
37
+
38
+ def data
39
+ setup_data
40
+ Data.new(@data)
41
+ end
42
+
43
+ private
44
+ def setup_data
45
+ @data ||= {:stores => []}
46
+ @data[:attribute_maps] ||= {}
47
+ @data[:included_attributes] ||= {}
48
+ end
49
+
50
+ class Data < OpenStruct
51
+ class ConfigurationError < NoMethodError; end
52
+
53
+ def method_missing(meth, *args)
54
+ return nil
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,27 @@
1
+ module MadCart
2
+ module InheritableAttributes
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def inheritable_attributes(*args)
9
+ @madcart_inheritable_attributes ||= [:inheritable_attributes]
10
+ @madcart_inheritable_attributes += args
11
+ args.each do |arg|
12
+ class_eval %(
13
+ class << self; attr_accessor :#{arg} end
14
+ )
15
+ end
16
+ @madcart_inheritable_attributes
17
+ end
18
+
19
+ def inherited(subclass)
20
+ @madcart_inheritable_attributes.each do |inheritable_attribute|
21
+ instance_var = "@#{inheritable_attribute}"
22
+ subclass.instance_variable_set(instance_var, instance_variable_get(instance_var))
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,68 @@
1
+ module MadCart
2
+ module Model
3
+ module Base
4
+ def initialize(args={})
5
+ self.additional_attributes = {}
6
+ check_required_attributes(args)
7
+ args.each { |k,v| set_attribute(k, v) }
8
+ end
9
+
10
+ def self.included(base)
11
+ base.extend ClassMethods
12
+ base.class_eval do
13
+ include AttributeMapper
14
+ include InheritableAttributes
15
+ attr_accessor :additional_attributes
16
+ inheritable_attributes :required_attrs
17
+ attr_accessor *exposed_attributes
18
+ end
19
+ end
20
+
21
+ def define_attribute_accessors
22
+ klass.class_eval do
23
+ attr_accessor(*exposed_attributes)
24
+ end
25
+ end
26
+
27
+ def check_required_attributes(args)
28
+ keys = args.keys.map{|a| a.to_s }
29
+ klass.required_attrs.each do |attr|
30
+ raise(ArgumentError, "missing argument: #{attr}") if !keys.include?(attr)
31
+ end
32
+ end
33
+ private :check_required_attributes
34
+
35
+ def set_attribute(key, value)
36
+ attr_name = klass.map_attribute_name(key)
37
+
38
+ if klass.exposed_attributes.include? attr_name.to_s
39
+ define_attribute_accessors unless self.respond_to?(attr_name)
40
+ self.send("#{attr_name}=", value) unless value.nil?
41
+ else
42
+ self.additional_attributes[attr_name.to_s] = value unless value.nil?
43
+ end
44
+ end
45
+ private :set_attribute
46
+
47
+ def klass
48
+ self.class
49
+ end
50
+ private :klass
51
+
52
+ module ClassMethods
53
+ def required_attributes(*args)
54
+ @required_attrs = args.map{|a| a.to_s }
55
+ attr_accessor *args
56
+ end
57
+
58
+ def exposed_attributes
59
+ ((self.required_attrs || []) + included_attributes + mapped_attributes).uniq.map{|a| a.to_s } - unmapped_attributes.map{|a| a.to_s }
60
+ end
61
+
62
+ def included_attributes
63
+ MadCart.config.included_attributes[self.to_s.demodulize.underscore.pluralize.to_sym] || []
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,9 @@
1
+ module MadCart
2
+ module Model
3
+ class Customer
4
+ include MadCart::Model::Base
5
+
6
+ required_attributes :first_name, :last_name, :email
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module MadCart
2
+ module Model
3
+ class Product
4
+ include MadCart::Model::Base
5
+
6
+ required_attributes :name, :description, :image_url
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,151 @@
1
+ module MadCart
2
+ module Store
3
+ class SetupError < StandardError
4
+ def self.message
5
+ "It appears MyStore has overrided the default "\
6
+ "MadCart::Base initialize method. That's fine, but please store "\
7
+ "any required connection arguments as @init_args for the "\
8
+ "#connection method to use later. Remember to call #after_initialize "\
9
+ "in your initialize method should you require it."
10
+ end
11
+ end
12
+ class InvalidStore < StandardError; end
13
+
14
+ module Base
15
+ def self.included(base)
16
+ base.extend ClassMethods
17
+ base.class_eval do
18
+ include InheritableAttributes
19
+ inheritable_attributes :connection_delegate, :required_connection_args,
20
+ :fetch_delegates, :format_delegates, :after_init_delegate
21
+ end
22
+ end
23
+
24
+ def initialize(*args)
25
+ set_init_args(*args)
26
+ after_initialize(*args)
27
+ end
28
+
29
+ def connection
30
+ validate_connection_args!
31
+ return init_connection
32
+ end
33
+
34
+ def init_connection
35
+ @connection ||= execute_delegate(klass.connection_delegate, @init_args)
36
+ end
37
+
38
+ def klass
39
+ self.class
40
+ end
41
+ private :klass
42
+
43
+ def execute_delegate(delegate, *args)
44
+ return self.send(delegate, *args) if delegate.is_a?(Symbol)
45
+ return delegate.call(*args) if delegate.is_a?(Proc)
46
+
47
+ raise ArgumentError, "Invalid delegate" # if not returned by now
48
+ end
49
+ private :execute_delegate
50
+
51
+ def after_initialize(*args)
52
+ return nil unless klass.after_init_delegate
53
+ execute_delegate(klass.after_init_delegate, *args)
54
+ end
55
+ private :after_initialize
56
+
57
+ def validate_connection_args!
58
+ return true if klass.required_connection_args.empty?
59
+
60
+ raise(SetupError, SetupError.message) if @init_args.nil?
61
+ raise(ArgumentError,"Missing connection arguments: "\
62
+ "#{missing_args.join(', ')}") if missing_args.present?
63
+ end
64
+ private :validate_connection_args!
65
+
66
+ def missing_args
67
+ klass.required_connection_args - @init_args.keys
68
+ end
69
+ private :missing_args
70
+
71
+ def set_init_args(*args)
72
+ @init_args ||= configured_connection_args.merge(args.last || {})
73
+ end
74
+ private :set_init_args
75
+
76
+ def configured_connection_args
77
+ MadCart.config.send(klass.to_s.demodulize.underscore) || {}
78
+ end
79
+ private :configured_connection_args
80
+
81
+ def ensure_model_format(model, results)
82
+ if results.first.is_a?(MadCart::Model::Base)
83
+ results
84
+ else
85
+ map_to_madcart_model(model, results)
86
+ end
87
+ end
88
+ private :ensure_model_format
89
+
90
+ def map_to_madcart_model(model, results)
91
+ results.map do |args|
92
+ "MadCart::Model::#{model.to_s.classify}".constantize.new(args)
93
+ end
94
+ end
95
+ private :map_to_madcart_model
96
+
97
+ module ClassMethods
98
+ def create_connection_with(*args)
99
+ @connection_delegate = parse_delegate(args.first, :create_connection_with)
100
+ opts = args[1] || {}
101
+ @required_connection_args = opts[:requires] || []
102
+ end
103
+
104
+ def fetch(model, options={})
105
+ @fetch_delegates ||= {}
106
+ @format_delegates ||= {}
107
+ @fetch_delegates[model] = parse_delegate(options, :fetch)
108
+
109
+ define_method_for(model)
110
+ end
111
+
112
+ def format(model, options={})
113
+ if @fetch_delegates[model].nil?
114
+ raise ArgumentError, "Cannot define 'format' for a model that has not defined 'fetch'"
115
+ end
116
+
117
+ @format_delegates[model] = parse_delegate(options, :format)
118
+ end
119
+
120
+ def after_initialize(*args)
121
+ @after_init_delegate = parse_delegate(args.first, :after_initialize)
122
+ end
123
+
124
+ def parse_delegate(arg, method)
125
+ return arg if (arg.is_a?(Symbol) || arg.is_a?(Proc))
126
+ return arg[:with] if arg.is_a?(Hash) && arg[:with]
127
+
128
+ raise ArgumentError, "Invalid delegate for #{method}: "\
129
+ "#{arg.first.class}. Use Proc or Symbol. "
130
+ end
131
+ private :parse_delegate
132
+
133
+ def define_method_for(model)
134
+ define_method model do |*args|
135
+ fetch_result = execute_delegate(self.class.fetch_delegates[model], *args)
136
+ formatted_result = if self.class.format_delegates[model]
137
+ formatter = self.class.format_delegates[model]
138
+ fetch_result.map{|r| execute_delegate(formatter, r)}
139
+ else
140
+ fetch_result
141
+ end
142
+
143
+ return ensure_model_format(model, formatted_result)
144
+ end
145
+ end
146
+ private :define_method_for
147
+
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,118 @@
1
+ module MadCart
2
+ module Store
3
+ class BigCommerce
4
+ class InvalidStore < StandardError; end;
5
+ class ServerError < StandardError; end;
6
+ class InvalidCredentials < StandardError; end;
7
+
8
+ include MadCart::Store::Base
9
+
10
+ create_connection_with :create_connection, :requires => [:api_key, :store_url, :username]
11
+ fetch :customers, :with => :get_customer_hashes
12
+ fetch :products, :with => :get_products
13
+
14
+ def valid?
15
+ check_for_errors do
16
+ connection.get('time.json')
17
+ end
18
+ return true
19
+
20
+ rescue InvalidCredentials => e
21
+ return false
22
+ end
23
+
24
+ def products_count
25
+ (parse_response { connection.get('products/count.json') })["count"]
26
+ end
27
+
28
+ private
29
+
30
+ def make_customer_request(params={:min_id => 1})
31
+ parse_response { connection.get('customers.json', params) }
32
+ end
33
+
34
+ def get_products(options={})
35
+ product_hashes = connection.get('products.json', options).try(:body)
36
+ return [] unless product_hashes
37
+
38
+ threads, images = [], []
39
+ product_hashes.each do |product|
40
+ threads << Thread.new do
41
+ if product["images"]
42
+ url = "#{product["images"]["resource"][1..-1]}.json"
43
+ images << parse_response { connection.get(url) }
44
+ end
45
+ end
46
+ end
47
+ threads.each { |t| t.join }
48
+
49
+ product_hashes.map do |p|
50
+
51
+ product_images = images.find { |i| i.first['product_id'] == p['id'] }
52
+ thumbnail = product_images.find { |i| i["is_thumbnail"] }
53
+ image = product_images.sort_by{|i| i["sort_order"] }.find { |i| i["is_thumbnail"] }
54
+
55
+ p.merge({
56
+ :image_square_url => connection.build_url("/product_images/#{thumbnail['image_file']}").to_s,
57
+ :image_url => connection.build_url("/product_images/#{image['image_file']}").to_s,
58
+ })
59
+ end
60
+ end
61
+
62
+ def get_customer_hashes
63
+ result = []
64
+ loop(:make_customer_request) {|c| result << c }
65
+ return result
66
+ end
67
+
68
+ def loop(source, &block)
69
+
70
+ items = send(source, :min_id => 1)
71
+
72
+ while true
73
+ items.each &block
74
+ break if items.count < 200
75
+ items = send(source, :min_id => items.max_id + 1 )
76
+ end
77
+
78
+ end
79
+
80
+ def parse_response(&block)
81
+ response = check_for_errors &block
82
+ return [] if empty_body?(response)
83
+ return response.body
84
+ end
85
+
86
+ def check_for_errors(&block)
87
+ response = yield
88
+
89
+ case response.status
90
+ when 401
91
+ raise InvalidCredentials
92
+ when 500
93
+ raise ServerError
94
+ end
95
+
96
+ response
97
+ rescue Faraday::Error::ConnectionFailed => e
98
+ raise InvalidStore
99
+ end
100
+
101
+ def api_url_for(store_domain)
102
+ "https://#{store_domain}/api/v2/"
103
+ end
104
+
105
+ def empty_body?(response)
106
+ true if response.status == 204 || response.body.nil?
107
+ end
108
+
109
+ def create_connection(args={})
110
+ @connection = Faraday.new(:url => api_url_for(args[:store_url]))
111
+ @connection.basic_auth(args[:username], args[:api_key])
112
+ @connection.response :json
113
+ @connection.adapter Faraday.default_adapter
114
+ @connection
115
+ end
116
+ end
117
+ end
118
+ end