etsy 0.2.0 → 0.2.1

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