ecwid_api 0.0.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +8 -0
  3. data/README.md +123 -31
  4. data/ecwid_api.gemspec +10 -8
  5. data/lib/ecwid_api.rb +18 -29
  6. data/lib/ecwid_api/api.rb +12 -0
  7. data/lib/ecwid_api/api/base.rb +18 -0
  8. data/lib/ecwid_api/api/categories.rb +56 -0
  9. data/lib/ecwid_api/api/customers.rb +53 -0
  10. data/lib/ecwid_api/api/orders.rb +36 -0
  11. data/lib/ecwid_api/api/product_combinations.rb +48 -0
  12. data/lib/ecwid_api/api/product_types.rb +56 -0
  13. data/lib/ecwid_api/api/products.rb +148 -0
  14. data/lib/ecwid_api/category.rb +53 -4
  15. data/lib/ecwid_api/client.rb +65 -58
  16. data/lib/ecwid_api/customer.rb +10 -0
  17. data/lib/ecwid_api/entity.rb +151 -29
  18. data/lib/ecwid_api/error.rb +10 -0
  19. data/lib/ecwid_api/o_auth.rb +106 -0
  20. data/lib/ecwid_api/order.rb +118 -0
  21. data/lib/ecwid_api/order_item.rb +17 -0
  22. data/lib/ecwid_api/paged_ecwid_response.rb +57 -0
  23. data/lib/ecwid_api/paged_enumerator.rb +66 -0
  24. data/lib/ecwid_api/person.rb +7 -0
  25. data/lib/ecwid_api/product.rb +65 -0
  26. data/lib/ecwid_api/product_combination.rb +30 -0
  27. data/lib/ecwid_api/product_type.rb +18 -0
  28. data/lib/ecwid_api/product_type_attribute.rb +27 -0
  29. data/lib/ecwid_api/unpaged_ecwid_response.rb +38 -0
  30. data/lib/ecwid_api/version.rb +1 -1
  31. data/lib/ext/string.rb +9 -1
  32. data/spec/api/categories_spec.rb +31 -0
  33. data/spec/api/customers_spec.rb +20 -0
  34. data/spec/api/orders_spec.rb +30 -0
  35. data/spec/api/product_types_spec.rb +20 -0
  36. data/spec/api/products_spec.rb +20 -0
  37. data/spec/category_spec.rb +1 -6
  38. data/spec/client_spec.rb +4 -32
  39. data/spec/entity_spec.rb +120 -8
  40. data/spec/fixtures/categories.json +28 -22
  41. data/spec/fixtures/classes.json +44 -0
  42. data/spec/fixtures/customers.json +48 -0
  43. data/spec/fixtures/order.json +162 -0
  44. data/spec/fixtures/orders.json +303 -0
  45. data/spec/fixtures/products.json +141 -0
  46. data/spec/helpers/client.rb +34 -0
  47. data/spec/oauth_spec.rb +40 -0
  48. data/spec/order_item_spec.rb +12 -0
  49. data/spec/order_spec.rb +71 -0
  50. data/spec/paged_enumerator_spec.rb +38 -0
  51. data/spec/spec_helper.rb +3 -3
  52. metadata +93 -37
  53. data/lib/ecwid_api/category_api.rb +0 -62
  54. data/spec/category_api_spec.rb +0 -36
  55. data/spec/ecwid_api_spec.rb +0 -15
  56. data/spec/helpers/faraday.rb +0 -30
@@ -0,0 +1,10 @@
1
+ module EcwidApi
2
+ class Customer < Entity
3
+ self.url_root = "customers"
4
+
5
+ ecwid_reader :name, :email, :totalOrderCount, :customerGroupId, :customerGroupName
6
+
7
+ ecwid_writer :name, :email
8
+
9
+ end
10
+ end
@@ -1,12 +1,87 @@
1
1
  module EcwidApi
2
2
  class Entity
3
- # Private: Gets the Client
3
+ # Private: Gets the Hash of data
4
+ attr_reader :data, :new_data
5
+ protected :data, :new_data
6
+
4
7
  attr_reader :client
5
- private :client
8
+ private :client
6
9
 
7
- # Private: Gets the Hash of data
8
- attr_reader :data
9
- private :data
10
+ class << self
11
+ attr_accessor :url_root
12
+
13
+ def define_accessor(attribute, &block)
14
+ if const_defined?(:Accessors, false)
15
+ mod = const_get(:Accessors)
16
+ else
17
+ mod = const_set(:Accessors, Module.new)
18
+ include mod
19
+ end
20
+
21
+ mod.module_eval do
22
+ define_method(attribute, &block)
23
+ end
24
+ end
25
+
26
+ private :define_accessor
27
+
28
+ # Public: Creates a snake_case access method from an Ecwid property name
29
+ #
30
+ # Example
31
+ #
32
+ # class Product < Entity
33
+ # ecwid_reader :id, :inStock
34
+ # end
35
+ #
36
+ # product = client.products.find(12)
37
+ # product.in_stock
38
+ #
39
+ def ecwid_reader(*attrs)
40
+ attrs.map(&:to_s).each do |attribute|
41
+ method = attribute.underscore
42
+ define_accessor(method) do
43
+ self[attribute]
44
+ end unless method_defined?(attribute.underscore)
45
+ end
46
+ end
47
+
48
+ # Public: Creates a snake_case writer method from an Ecwid property name
49
+ #
50
+ # Example
51
+ #
52
+ # class Product < Entity
53
+ # ecwid_writer :inStock
54
+ # end
55
+ #
56
+ # product = client.products.find(12)
57
+ # product.in_stock = true
58
+ #
59
+ def ecwid_writer(*attrs)
60
+ attrs.map(&:to_s).each do |attribute|
61
+ method = "#{attribute.underscore}="
62
+ define_accessor(method) do |value|
63
+ @new_data[attribute] = value
64
+ end unless method_defined?(method)
65
+ end
66
+ end
67
+
68
+ # Public: Creates a snake_case accessor method from an Ecwid property name
69
+ #
70
+ # Example
71
+ #
72
+ # class Product < Entity
73
+ # ecwid_accessor :inStock
74
+ # end
75
+ #
76
+ # product = client.products.find(12)
77
+ # product.in_stock
78
+ # product.in_stock = true
79
+ #
80
+ def ecwid_accessor(*attrs)
81
+ ecwid_reader(*attrs)
82
+ ecwid_writer(*attrs)
83
+ end
84
+ end
10
85
 
11
86
  # Public: Initialize a new entity with a reference to the client and data
12
87
  #
@@ -16,6 +91,7 @@ module EcwidApi
16
91
  #
17
92
  def initialize(data, opts={})
18
93
  @client, @data = opts[:client], data
94
+ @new_data = {}
19
95
  end
20
96
 
21
97
  # Public: Returns a property of the data (actual property name)
@@ -31,37 +107,83 @@ module EcwidApi
31
107
  #
32
108
  # Returns the value of the property, or nil if it doesn't exist
33
109
  def [](key)
34
- data[key.to_s]
110
+ @new_data[key.to_s] || data[key.to_s]
35
111
  end
36
112
 
37
- # Public: Get a property of the data (snake_case)
113
+ # Public: The URL of the entity
38
114
  #
39
- # This is used as a helper to allow easy access to the data. It will work
40
- # with both CamelCased and snake_case keys. For example, if the data
41
- # contains a "parentId" key, then calling `entity.parent_id` should work.
42
- #
43
- # This will NOT return null of the property doesn't exist on the data!
44
- #
45
- # Examples
115
+ # Returns a String that is the URL of the entity
116
+ def url
117
+ url_root = self.class.url_root
118
+ raise Error.new("Please specify a url_root for the #{self.class.to_s}") unless url_root
119
+
120
+ if url_root.respond_to?(:call)
121
+ url_root = instance_exec(&url_root)
122
+ end
123
+
124
+ url_root + "/#{id}"
125
+ end
126
+
127
+ def assign_attributes(attributes)
128
+ attributes.each do |key, val|
129
+ send("#{key}=", val)
130
+ end
131
+ end
132
+
133
+ def assign_raw_attributes(attributes)
134
+ attributes.each do |key, val|
135
+ @new_data[key.to_s] = val
136
+ end
137
+ end
138
+
139
+ def update_attributes(attributes)
140
+ assign_attributes(attributes)
141
+ save
142
+ end
143
+
144
+ def update_raw_attributes(attributes)
145
+ assign_raw_attributes(attributes)
146
+ save
147
+ end
148
+
149
+ # Public: Saves the Entity
46
150
  #
47
- # entity.parent_id # same as `entity["parentId"]`
151
+ # Saves anything stored in the @new_data hash
48
152
  #
49
- # TODO: #method_missing isn't the ideal solution because Ecwid will only
50
- # return a property if it doesn't have a null value. An example of this are
51
- # the top level categories. They don't have a parentId, so that property
52
- # is ommitted from the API response. Calling `category.parent_id` will
53
- # result in an "undefined method `parent_id'". However, calling `#parent_id`
54
- # on any other category will work.
153
+ # path - the URL of the entity
55
154
  #
56
- # Returns the value of the property
57
- def method_missing(method, *args)
58
- method_string = method.to_s
59
-
60
- [ method_string, method_string.camel_case ].each do |key|
61
- return data[key] if data.has_key?(key)
155
+ def save
156
+ unless @new_data.empty?
157
+ client.put(url, @new_data).tap do |response|
158
+ @data.merge!(@new_data)
159
+ @new_data.clear
160
+ end
62
161
  end
162
+ end
163
+
164
+ # Public: Destroys the Entity
165
+ def destroy!
166
+ client.delete(url)
167
+ end
168
+
169
+ def to_hash
170
+ data
171
+ end
172
+
173
+ def to_json(*args)
174
+ data.to_json(*args)
175
+ end
176
+
177
+ def marshal_dump
178
+ [@data, @new_data]
179
+ end
180
+
181
+ def marshal_load(array)
182
+ @data, @new_data = *array
183
+ end
63
184
 
64
- super method, *args
185
+ def ==(other)
186
+ data == other.data && new_data == other.new_data
65
187
  end
66
188
  end
67
- end
189
+ end
@@ -1,3 +1,13 @@
1
1
  module EcwidApi
2
2
  class Error < StandardError; end;
3
+
4
+ class ResponseError < Error
5
+ def initialize(response)
6
+ if response.respond_to?(:reason_phrase)
7
+ super "#{response.reason_phrase} (#{response.status})\n#{response.body}"
8
+ else
9
+ super "The Ecwid API responded with an error (#{response.status})"
10
+ end
11
+ end
12
+ end
3
13
  end
@@ -0,0 +1,106 @@
1
+ require "cgi"
2
+ require "ostruct"
3
+
4
+ module EcwidApi
5
+ # Public: Authentication objects manage OAuth authentication with an Ecwid
6
+ # store.
7
+ #
8
+ # Examples
9
+ #
10
+ # app = EcwidApi::Authentication.new do |config|
11
+ # # see initialize for configuration
12
+ # end
13
+ #
14
+ # app.oauth_url # send the user here to authorize the app
15
+ #
16
+ # token = app.access_token(params[:code]) # this is the code they provide
17
+ # # to the redirect_uri
18
+ # token.access_token
19
+ # token.store_id # these are what you need to access the API
20
+ #
21
+ class OAuth
22
+ CONFIG = %w(client_id client_secret scope redirect_uri)
23
+ attr_accessor *CONFIG
24
+
25
+ # Public: Initializes a new Ecwid Authentication for OAuth
26
+ #
27
+ # Examples
28
+ #
29
+ # app = EcwidApi::Authentication.new do |config|
30
+ # config.client_id = "some client id"
31
+ # config.client_secret = "some client secret"
32
+ # config.scope "this_is_what_i_want_to_do oh_and_that_too"
33
+ # config.redirect_uri = "https://example.com/oauth"
34
+ # end
35
+ #
36
+ def initialize
37
+ yield(self) if block_given?
38
+ CONFIG.each do |method|
39
+ raise Error.new("#{method} is required to initialize a new EcwidApi::Authentication") unless send(method)
40
+ end
41
+ end
42
+
43
+ # Public: The URL for OAuth authorization.
44
+ #
45
+ # This is the URL that the user will need to go to to authorize the app
46
+ # with the Ecwid store.
47
+ #
48
+ def oauth_url
49
+ "https://my.ecwid.com/api/oauth/authorize?" + oauth_query
50
+ end
51
+
52
+ # Public: Obtain the access token in order to use the API
53
+ #
54
+ # code - the temporary code obtained from the authorization callback
55
+ #
56
+ # Examples
57
+ #
58
+ # token = app.access_token(params[:code])
59
+ # token.access_token # the access token that authenticates each API request
60
+ # token.store_id # the authenticated Ecwid store_id
61
+ #
62
+ # Returns an OpenStruct which responds with the information needed to
63
+ # access the API for a store.
64
+ def access_token(code)
65
+ response = connection.post("/api/oauth/token",
66
+ client_id: client_id,
67
+ client_secret: client_secret,
68
+ code: code,
69
+ redirect_uri: redirect_uri,
70
+ grant_type: "authorization_code"
71
+ )
72
+
73
+ if response.success?
74
+ OpenStruct.new(response.body)
75
+ else
76
+ raise Error.new(response.body["error_description"])
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ # Private: The query parameters for the OAuth authorization request
83
+ #
84
+ # Returns a String of query parameters
85
+ def oauth_query
86
+ {
87
+ client_id: client_id,
88
+ scope: scope,
89
+ response_type: "code",
90
+ redirect_uri: redirect_uri
91
+ }.map do |key, val|
92
+ "#{CGI.escape(key.to_s)}=#{CGI.escape(val.to_s)}"
93
+ end.join(?&)
94
+ end
95
+
96
+ # Private: Returns a connection for obtaining an access token from Ecwid
97
+ #
98
+ def connection
99
+ @connection ||= Faraday.new "https://my.ecwid.com" do |conn|
100
+ conn.request :url_encoded
101
+ conn.response :json, content_type: /\bjson$/
102
+ conn.adapter Faraday.default_adapter
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,118 @@
1
+ module EcwidApi
2
+ # Public: This is an Ecwid Order
3
+ class Order < Entity
4
+ self.url_root = "orders"
5
+
6
+ ecwid_reader :id, :orderNumber, :vendorOrderNumber, :subtotal, :total, :email,
7
+ :paymentMethod, :paymentModule, :tax, :ipAddress,
8
+ :couponDiscount, :paymentStatus, :fulfillmentStatus,
9
+ :refererUrl, :orderComments, :volumeDiscount, :customerId,
10
+ :membershipBasedDiscount, :totalAndMembershipBasedDiscount,
11
+ :discount, :usdTotal, :globalReferer, :createDate, :updateDate,
12
+ :customerGroup, :discountCoupon, :items, :billingPerson,
13
+ :shippingPerson, :shippingOption, :additionalInfo,
14
+ :paymentParams, :discountInfo, :trackingNumber,
15
+ :paymentMessage, :extTransactionId, :affiliateId,
16
+ :creditCardStatus, :handlingFee
17
+
18
+
19
+ ecwid_writer :subtotal, :total, :email, :paymentMethod, :paymentModule,
20
+ :tax, :ipAddress, :couponDiscount, :paymentStatus,
21
+ :fulfillmentStatus, :refererUrl, :orderComments,
22
+ :volumeDiscount, :customerId, :membershipBasedDiscount,
23
+ :totalAndMembershipBasedDiscount, :discount, :globalReferer,
24
+ :createDate, :updateDate, :customerGroup, :discountCoupon,
25
+ :items, :billingPerson, :shippingPerson, :shippingOption,
26
+ :additionalInfo, :paymentParams, :discountInfo,
27
+ :trackingNumber, :paymentMessage, :extTransactionId,
28
+ :affiliateId, :creditCardStatus, :handlingFee
29
+
30
+ VALID_FULFILLMENT_STATUSES = %w(
31
+ AWAITING_PROCESSING
32
+ PROCESSING
33
+ SHIPPED
34
+ DELIVERED
35
+ WILL_NOT_DELIVER
36
+ RETURNED
37
+ READY_FOR_PICKUP
38
+ OUT_FOR_DELIVERY
39
+ ).freeze
40
+
41
+ VALID_PAYMENT_STATUSES = %w(
42
+ AWAITING_PAYMENT
43
+ PAID
44
+ CANCELLED
45
+ REFUNDED
46
+ PARTIALLY_REFUNDED
47
+ INCOMPLETE
48
+ ).freeze
49
+
50
+ # @deprecated Please use {#id} instead
51
+ def vendor_order_number
52
+ warn "[DEPRECATION] `vendor_order_number` is deprecated. Please use `id` instead."
53
+ id
54
+ end
55
+
56
+ # @deprecated Please use {#id} instead
57
+ def order_number
58
+ warn "[DEPRECATION] `order_number` is deprecated. Please use `id` instead."
59
+ id
60
+ end
61
+
62
+ # Public: Returns the billing person
63
+ #
64
+ # If there isn't a billing_person, then it assumed to be the shipping_person
65
+ #
66
+ def billing_person
67
+ build_billing_person || build_shipping_person
68
+ end
69
+
70
+ # Public: Returns the shipping person
71
+ #
72
+ # If there isn't a shipping_person, then it is assumed to be the
73
+ # billing_person
74
+ #
75
+ def shipping_person
76
+ build_shipping_person || build_billing_person
77
+ end
78
+
79
+ # Public: Returns a Array of `OrderItem` objects
80
+ def items
81
+ @items ||= data["items"].map { |item| OrderItem.new(item) }
82
+ end
83
+
84
+ def fulfillment_status=(status)
85
+ status = status.to_s.upcase
86
+ unless VALID_FULFILLMENT_STATUSES.include?(status)
87
+ raise Error("#{status} is an invalid fullfillment status")
88
+ end
89
+ super(status)
90
+ end
91
+
92
+ def fulfillment_status
93
+ super && super.downcase.to_sym
94
+ end
95
+
96
+ def payment_status=(status)
97
+ status = status.to_s.upcase
98
+ unless VALID_PAYMENT_STATUSES.include?(status)
99
+ raise Error("#{status} is an invalid payment status")
100
+ end
101
+ super(status)
102
+ end
103
+
104
+ def payment_status
105
+ super && super.downcase.to_sym
106
+ end
107
+
108
+ private
109
+
110
+ def build_billing_person
111
+ @billing_person ||= data["billingPerson"] && Person.new(data["billingPerson"])
112
+ end
113
+
114
+ def build_shipping_person
115
+ @shipping_person ||= data["shippingPerson"] && Person.new(data["shippingPerson"])
116
+ end
117
+ end
118
+ end