etsy 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/.gitignore +8 -0
  2. data/.travis.yml +8 -0
  3. data/Gemfile +10 -0
  4. data/README.md +300 -0
  5. data/Rakefile +2 -30
  6. data/etsy.gemspec +36 -0
  7. data/lib/etsy.rb +46 -17
  8. data/lib/etsy/address.rb +47 -0
  9. data/lib/etsy/basic_client.rb +1 -1
  10. data/lib/etsy/category.rb +84 -0
  11. data/lib/etsy/country.rb +27 -0
  12. data/lib/etsy/image.rb +7 -3
  13. data/lib/etsy/listing.rb +107 -8
  14. data/lib/etsy/model.rb +99 -3
  15. data/lib/etsy/payment_template.rb +33 -0
  16. data/lib/etsy/profile.rb +49 -0
  17. data/lib/etsy/request.rb +85 -17
  18. data/lib/etsy/response.rb +80 -4
  19. data/lib/etsy/section.rb +16 -0
  20. data/lib/etsy/secure_client.rb +49 -4
  21. data/lib/etsy/shipping_template.rb +32 -0
  22. data/lib/etsy/shop.rb +21 -12
  23. data/lib/etsy/transaction.rb +18 -0
  24. data/lib/etsy/user.rb +45 -13
  25. data/lib/etsy/verification_request.rb +2 -2
  26. data/test/fixtures/address/getUserAddresses.json +12 -0
  27. data/test/fixtures/category/findAllSubCategoryChildren.json +78 -0
  28. data/test/fixtures/category/findAllTopCategory.json +347 -0
  29. data/test/fixtures/category/findAllTopCategory.single.json +18 -0
  30. data/test/fixtures/category/findAllTopCategoryChildren.json +308 -0
  31. data/test/fixtures/category/getCategory.multiple.json +28 -0
  32. data/test/fixtures/category/getCategory.single.json +18 -0
  33. data/test/fixtures/country/getCountry.json +1 -0
  34. data/test/fixtures/listing/findAllListingActive.category.json +827 -0
  35. data/test/fixtures/listing/{findAllShopListingsActive.json → findAllShopListings.json} +0 -0
  36. data/test/fixtures/listing/getListing.multiple.json +1 -0
  37. data/test/fixtures/listing/getListing.single.json +1 -0
  38. data/test/fixtures/payment_template/getPaymentTemplate.json +1 -0
  39. data/test/fixtures/profile/new.json +28 -0
  40. data/test/fixtures/section/getShopSection.json +18 -0
  41. data/test/fixtures/shipping_template/getShippingTemplate.json +1 -0
  42. data/test/fixtures/shop/getShop.single.json +4 -3
  43. data/test/fixtures/transaction/findAllShopTransactions.json +1 -0
  44. data/test/fixtures/user/getUser.single.withProfile.json +38 -0
  45. data/test/fixtures/user/getUser.single.withShops.json +41 -0
  46. data/test/test_helper.rb +9 -4
  47. data/test/unit/etsy/address_test.rb +61 -0
  48. data/test/unit/etsy/category_test.rb +106 -0
  49. data/test/unit/etsy/country_test.rb +64 -0
  50. data/test/unit/etsy/listing_test.rb +112 -5
  51. data/test/unit/etsy/model_test.rb +64 -0
  52. data/test/unit/etsy/payment_template_test.rb +68 -0
  53. data/test/unit/etsy/profile_test.rb +111 -0
  54. data/test/unit/etsy/request_test.rb +89 -53
  55. data/test/unit/etsy/response_test.rb +118 -4
  56. data/test/unit/etsy/section_test.rb +28 -0
  57. data/test/unit/etsy/secure_client_test.rb +27 -5
  58. data/test/unit/etsy/shipping_template_test.rb +24 -0
  59. data/test/unit/etsy/shop_test.rb +12 -5
  60. data/test/unit/etsy/transaction_test.rb +52 -0
  61. data/test/unit/etsy/user_test.rb +147 -8
  62. data/test/unit/etsy/verification_request_test.rb +3 -3
  63. data/test/unit/etsy_test.rb +19 -7
  64. metadata +133 -77
  65. data/README.rdoc +0 -208
  66. data/lib/etsy/version.rb +0 -13
@@ -0,0 +1,33 @@
1
+ module Etsy
2
+ class PaymentTemplate
3
+ include Etsy::Model
4
+
5
+ attribute :id, :from => :payment_template_id
6
+ attributes :allow_check, :allow_mo, :allow_other, :allow_paypal, :allow_cc
7
+ attributes :paypal_email, :name, :first_line, :second_line, :city, :state
8
+ attributes :zip, :country_id
9
+
10
+ def self.create(options = {})
11
+ options.merge!(:require_secure => true)
12
+ post("/payments/templates", options)
13
+ end
14
+
15
+ def self.find(id, credentials = {})
16
+ options = {
17
+ :access_token => credentials[:access_token],
18
+ :access_secret => credentials[:access_secret],
19
+ :require_secure => true
20
+ }
21
+ get("/payments/templates/#{id}", options)
22
+ end
23
+
24
+ def self.find_by_user(user, credentials = {})
25
+ options = {
26
+ :access_token => credentials[:access_token],
27
+ :access_secret => credentials[:access_secret],
28
+ :require_secure => true
29
+ }
30
+ get("/users/#{user.id}/payments/templates", options)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,49 @@
1
+ module Etsy
2
+
3
+ # = Profile
4
+ #
5
+ # Represents a profile resource of an Etsy user.
6
+ #
7
+ class Profile
8
+
9
+ include Etsy::Model
10
+
11
+ attribute :id, :from => :user_profile_id
12
+ attribute :user_id
13
+ attribute :bio
14
+ attribute :username, :from => :login_name
15
+ attribute :gender
16
+ attribute :birth_day
17
+ attribute :birth_month
18
+ attribute :birth_year
19
+ attribute :joined, :from => :join_tsz
20
+ attribute :favorite_materials, :from => :materials
21
+ attribute :country_id
22
+ attribute :city
23
+ attribute :location
24
+ attribute :region
25
+ attribute :avatar_id
26
+ attribute :image, :from => :image_url_75x75
27
+ attribute :lat
28
+ attribute :lon
29
+ attribute :transaction_buy_count
30
+ attribute :transaction_sold_count
31
+ attribute :is_seller
32
+ attribute :first_name
33
+ attribute :last_name
34
+
35
+ def materials
36
+ favorite_materials ? favorite_materials.split(',') : []
37
+ end
38
+
39
+ # Time that this user joined Etsy
40
+ #
41
+ def joined_at
42
+ Time.at(joined)
43
+ end
44
+
45
+ def seller?
46
+ is_seller
47
+ end
48
+ end
49
+ end
data/lib/etsy/request.rb CHANGED
@@ -6,10 +6,6 @@ module Etsy
6
6
  #
7
7
  class Request
8
8
 
9
- def self.host # :nodoc:
10
- 'openapi.etsy.com'
11
- end
12
-
13
9
  # Perform a GET request for the resource with optional parameters - returns
14
10
  # A Response object with the payload data
15
11
  def self.get(resource_path, parameters = {})
@@ -17,18 +13,42 @@ module Etsy
17
13
  Response.new(request.get)
18
14
  end
19
15
 
16
+ def self.post(resource_path, parameters = {})
17
+ request = Request.new(resource_path, parameters)
18
+ Response.new(request.post)
19
+ end
20
+
21
+ def self.put(resource_path, parameters = {})
22
+ request = Request.new(resource_path, parameters)
23
+ Response.new(request.put)
24
+ end
25
+
20
26
  # Create a new request for the resource with optional parameters
21
27
  def initialize(resource_path, parameters = {})
28
+ parameters = parameters.dup
29
+ @token = parameters.delete(:access_token) || Etsy.credentials[:access_token]
30
+ @secret = parameters.delete(:access_secret) || Etsy.credentials[:access_secret]
31
+ raise("Secure connection required. Please provide your OAuth credentials via :access_token and :access_secret in the parameters") if parameters.delete(:require_secure) && !secure?
32
+ @multipart_request = parameters.delete(:multipart)
22
33
  @resource_path = resource_path
23
- @parameters = parameters
34
+ @resources = parameters.delete(:includes)
35
+ if @resources.class == String
36
+ @resources = @resources.split(',').map {|r| {:resource => r}}
37
+ elsif @resources.class == Array
38
+ @resources = @resources.map do |r|
39
+ if r.class == String
40
+ {:resource => r}
41
+ else
42
+ r
43
+ end
44
+ end
45
+ end
46
+ parameters = parameters.merge(:api_key => Etsy.api_key) unless secure?
47
+ @parameters = parameters
24
48
  end
25
49
 
26
50
  def base_path # :nodoc:
27
- parts = ['v2']
28
- parts << 'sandbox' if Etsy.environment == :sandbox
29
- parts << (secure? ? 'private' : 'public')
30
-
31
- "/#{parts.join('/')}"
51
+ "/v2"
32
52
  end
33
53
 
34
54
  # Perform a GET request against the API endpoint and return the raw
@@ -37,34 +57,82 @@ module Etsy
37
57
  client.get(endpoint_url)
38
58
  end
39
59
 
60
+ def post
61
+ if multipart?
62
+ client.post_multipart(endpoint_url(:include_query => false), @parameters)
63
+ else
64
+ client.post(endpoint_url)
65
+ end
66
+ end
67
+
68
+ def put
69
+ client.put(endpoint_url)
70
+ end
71
+
40
72
  def client # :nodoc:
41
73
  @client ||= secure? ? secure_client : basic_client
42
74
  end
43
75
 
44
76
  def parameters # :nodoc:
45
- @parameters.merge(:api_key => Etsy.api_key, :detail_level => 'high')
77
+ @parameters
78
+ end
79
+
80
+ def resources # :nodoc:
81
+ @resources
46
82
  end
47
83
 
48
84
  def query # :nodoc:
49
- parameters.map {|k,v| "#{k}=#{v}"}.join('&')
85
+ to_url(parameters.merge(:includes => resources.to_a.map { |r| association(r) }))
86
+ end
87
+
88
+ def to_url(val)
89
+ if val.is_a? Array
90
+ to_url(val.join(','))
91
+ elsif val.is_a? Hash
92
+ val.reject { |k, v|
93
+ k.nil? || v.nil? || (k.respond_to?(:empty?) && k.empty?) || (v.respond_to?(:empty?) && v.empty?)
94
+ }.map { |k, v| "#{to_url(k.to_s)}=#{to_url(v)}" }.join('&')
95
+ else
96
+ URI.escape(val.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
97
+ end
98
+ end
99
+
100
+ def association(options={}) # :nodoc:
101
+ s = options[:resource].dup
102
+
103
+ if options.include? :fields
104
+ s << "(#{[options[:fields]].flatten.join(',')})"
105
+ end
106
+
107
+ if options.include?(:limit) || options.include?(:offset)
108
+ s << ":#{options.fetch(:limit, 25)}:#{options.fetch(:offset, 0)}"
109
+ end
110
+
111
+ s
112
+ end
113
+
114
+ def endpoint_url(options = {}) # :nodoc:
115
+ url = "#{base_path}#{@resource_path}"
116
+ url += "?#{query}" if options.fetch(:include_query, true)
117
+ url
50
118
  end
51
119
 
52
- def endpoint_url # :nodoc:
53
- "#{base_path}#{@resource_path}?#{query}"
120
+ def multipart?
121
+ !!@multipart_request
54
122
  end
55
123
 
56
124
  private
57
125
 
58
126
  def secure_client
59
- SecureClient.new(:access_token => @parameters[:access_token], :access_secret => @parameters[:access_secret])
127
+ SecureClient.new(:access_token => @token, :access_secret => @secret)
60
128
  end
61
129
 
62
130
  def basic_client
63
- BasicClient.new(self.class.host)
131
+ BasicClient.new(Etsy.host)
64
132
  end
65
133
 
66
134
  def secure?
67
- Etsy.access_mode == :read_write && !@parameters[:access_token].nil? && !@parameters[:access_secret].nil?
135
+ !@token.nil? && !@secret.nil?
68
136
  end
69
137
 
70
138
  end
data/lib/etsy/response.rb CHANGED
@@ -1,5 +1,11 @@
1
1
  module Etsy
2
2
 
3
+ class OAuthTokenRevoked < StandardError; end
4
+ class MissingShopID < StandardError; end
5
+ class EtsyJSONInvalid < StandardError; end
6
+ class TemporaryIssue < StandardError; end
7
+ class InvalidUserID < StandardError; end
8
+
3
9
  # = Response
4
10
  #
5
11
  # Basic wrapper around the Etsy JSON response data
@@ -13,17 +19,43 @@ module Etsy
13
19
 
14
20
  # Convert the raw JSON data to a hash
15
21
  def to_hash
16
- @hash ||= JSON.parse(data)
22
+ validate!
23
+ @hash ||= json
24
+ end
25
+
26
+ def body
27
+ @raw_response.body
28
+ end
29
+
30
+ def code
31
+ @raw_response.code
17
32
  end
18
33
 
19
34
  # Number of records in the response results
20
35
  def count
21
- to_hash['count']
36
+ if paginated?
37
+ to_hash['results'].nil? ? 0 : to_hash['results'].size
38
+ else
39
+ to_hash['count']
40
+ end
22
41
  end
23
42
 
24
43
  # Results of the API request
25
44
  def result
26
- count == 1 ? to_hash['results'].first : to_hash['results']
45
+ if success?
46
+ results = to_hash['results'] || []
47
+ count == 1 ? results.first : results
48
+ else
49
+ []
50
+ end
51
+ end
52
+
53
+ def success?
54
+ !!(code =~ /2\d\d/)
55
+ end
56
+
57
+ def paginated?
58
+ !!to_hash['pagination']
27
59
  end
28
60
 
29
61
  private
@@ -32,5 +64,49 @@ module Etsy
32
64
  @raw_response.body
33
65
  end
34
66
 
67
+ def json
68
+ @hash ||= JSON.parse(data)
69
+ end
70
+
71
+ def validate!
72
+ raise OAuthTokenRevoked if token_revoked?
73
+ raise MissingShopID if missing_shop_id?
74
+ raise InvalidUserID if invalid_user_id?
75
+ raise TemporaryIssue if temporary_etsy_issue? || resource_unavailable? || exceeded_rate_limit?
76
+ raise EtsyJSONInvalid.new(data) unless valid_json?
77
+ true
78
+ end
79
+
80
+ def valid_json?
81
+ json
82
+ return true
83
+ rescue JSON::ParserError
84
+ return false
85
+ end
86
+
87
+ def token_revoked?
88
+ data == "oauth_problem=token_revoked"
89
+ end
90
+
91
+ def missing_shop_id?
92
+ data =~ /Shop with PK shop_id/
93
+ end
94
+
95
+ def invalid_user_id?
96
+ data =~ /is not a valid user_id/
97
+ end
98
+
99
+ def temporary_etsy_issue?
100
+ data =~ /Temporary Etsy issue/
101
+ end
102
+
103
+ def resource_unavailable?
104
+ data =~ /Resource temporarily unavailable/
105
+ end
106
+
107
+ def exceeded_rate_limit?
108
+ data =~ /You have exceeded/
109
+ end
110
+
35
111
  end
36
- end
112
+ end
@@ -0,0 +1,16 @@
1
+ module Etsy
2
+ class Section
3
+ include Etsy::Model
4
+
5
+ attributes :title, :rank, :user_id, :active_listing_count
6
+ attribute :id, :from => :shop_section_id
7
+
8
+ def self.find_by_shop(shop)
9
+ get("/shops/#{shop.id}/sections")
10
+ end
11
+
12
+ def self.find(shop, id)
13
+ get("/shops/#{shop.id}/sections/#{id}")
14
+ end
15
+ end
16
+ end
@@ -22,11 +22,11 @@ module Etsy
22
22
  end
23
23
 
24
24
  def consumer # :nodoc:
25
+ path = '/v2/oauth/'
25
26
  @consumer ||= OAuth::Consumer.new(Etsy.api_key, Etsy.api_secret, {
26
- :site => 'http://openapi.etsy.com',
27
- :request_token_path => '/v2/sandbox/oauth/request_token',
28
- :access_token_path => '/v2/sandbox/oauth/access_token',
29
- :authorize_url => 'https://www.etsy.com/oauth/signin'
27
+ :site => "http://#{Etsy.host}",
28
+ :request_token_path => "#{path}request_token?scope=#{Etsy.permission_scopes.join('+')}",
29
+ :access_token_path => "#{path}access_token"
30
30
  })
31
31
  end
32
32
 
@@ -69,8 +69,53 @@ module Etsy
69
69
  client.get(endpoint)
70
70
  end
71
71
 
72
+ def post(endpoint)
73
+ client.post(endpoint)
74
+ end
75
+
76
+ def put(endpoint)
77
+ client.put(endpoint)
78
+ end
79
+
80
+ def post_multipart(endpoint, params = {})
81
+ Net::HTTP.new(Etsy.host).start do |http|
82
+ req = Net::HTTP::Post.new(endpoint)
83
+ add_multipart_data(req, params)
84
+ add_oauth(req)
85
+ res = http.request(req)
86
+ end
87
+ end
88
+
72
89
  private
73
90
 
91
+ # Encodes the request as multipart
92
+ def add_multipart_data(req, params)
93
+ crlf = "\r\n"
94
+ boundary = Time.now.to_i.to_s(16)
95
+ req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
96
+ body = ""
97
+ params.each do |key,value|
98
+ esc_key = CGI.escape(key.to_s)
99
+ body << "--#{boundary}#{crlf}"
100
+ if value.respond_to?(:read)
101
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"; filename=\"#{File.basename(value.path)}\"#{crlf}"
102
+ body << "Content-Type: image/jpeg#{crlf*2}"
103
+ body << value.read
104
+ else
105
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"#{crlf*2}#{value}"
106
+ end
107
+ body << crlf
108
+ end
109
+ body << "--#{boundary}--#{crlf*2}"
110
+ req.body = body
111
+ req["Content-Length"] = req.body.size
112
+ end
113
+
114
+ # Uses the OAuth gem to add the signed Authorization header
115
+ def add_oauth(req)
116
+ client.sign!(req)
117
+ end
118
+
74
119
  def has_access_data?
75
120
  !@attributes[:access_token].nil? && !@attributes[:access_secret].nil?
76
121
  end
@@ -0,0 +1,32 @@
1
+ module Etsy
2
+ class ShippingTemplate
3
+ include Etsy::Model
4
+
5
+ attribute :id, :from => :shipping_template_id
6
+
7
+ attributes :title, :user_id
8
+
9
+ def self.create(options = {})
10
+ options.merge!(:require_secure => true)
11
+ post("/shipping/templates", options)
12
+ end
13
+
14
+ def self.find(id, credentials = {})
15
+ options = {
16
+ :access_token => credentials[:access_token],
17
+ :access_secret => credentials[:access_secret],
18
+ :require_secure => true
19
+ }
20
+ get("/shipping/templates/#{id}", options)
21
+ end
22
+
23
+ def self.find_by_user(user, credentials = {})
24
+ options = {
25
+ :access_token => credentials[:access_token],
26
+ :access_secret => credentials[:access_secret],
27
+ :require_secure => true
28
+ }
29
+ get("/users/#{user.id}/shipping/templates", options)
30
+ end
31
+ end
32
+ end