ecwid_api 0.0.2 → 0.2.3

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.
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