ebay_request 0.2.0

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.
@@ -0,0 +1,46 @@
1
+ # coding: utf-8
2
+ # frozen_string_literal: true
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "ebay_request/version"
6
+
7
+ # rubocop:disable Metrics/BlockLength
8
+ Gem::Specification.new do |spec|
9
+ spec.name = "ebay_request"
10
+ spec.version = EbayRequest::VERSION
11
+ spec.authors = ["Victor Sokolov"]
12
+ spec.email = ["gzigzigzeo@evilmartians.com"]
13
+
14
+ spec.summary = "eBay API request interface"
15
+ # spec.description = "TODO: Write a longer description or delete this line."
16
+ spec.homepage = "https://github.com/gzigzigzeo/ebay_request"
17
+
18
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
19
+ # delete this section to allow pushing this gem to any host.
20
+ if spec.respond_to?(:metadata)
21
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
22
+ else
23
+ raise "RubyGems 2.0 or newer is required to protect against public \
24
+ gem pushes."
25
+ end
26
+
27
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
28
+ f.match(%r{^(test|spec|features)/})
29
+ end
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ spec.add_development_dependency "bundler", "~> 1.11"
35
+ spec.add_development_dependency "rake", "~> 10.0"
36
+ spec.add_development_dependency "rspec", "~> 3.0"
37
+ spec.add_development_dependency "rubocop"
38
+ spec.add_development_dependency "webmock"
39
+ spec.add_development_dependency "simplecov"
40
+
41
+ spec.add_dependency "multi_xml"
42
+ spec.add_dependency "gyoku"
43
+ spec.add_dependency "omniauth"
44
+ spec.add_dependency "dry-initializer"
45
+ end
46
+ # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+ require "omniauth"
3
+ require "net/http"
4
+ require "gyoku"
5
+ require "multi_xml"
6
+ require "dry-initializer"
7
+
8
+ require "ebay_request/version"
9
+ require "ebay_request/site"
10
+ require "ebay_request/config"
11
+ require "ebay_request/base"
12
+ require "ebay_request/error"
13
+ require "ebay_request/finding"
14
+ require "ebay_request/shopping"
15
+ require "ebay_request/trading"
16
+ require "ebay_request/business_policies"
17
+ require "ebay_request/auth"
18
+ require "ebay_request/response"
19
+
20
+ require "omniauth/strategies/ebay"
21
+
22
+ module EbayRequest
23
+ class << self
24
+ attr_accessor :logger
25
+ attr_accessor :warn_logger
26
+ attr_accessor :config_repository
27
+ attr_accessor :json_logger
28
+
29
+ def config(key = nil)
30
+ @config_repository ||= {}
31
+ @config_repository[key || :default] ||= Config.new
32
+ end
33
+
34
+ def configure(key = nil)
35
+ yield(config(key)) && config(key)
36
+ end
37
+
38
+ def configured?
39
+ !@config_repository.nil?
40
+ end
41
+
42
+ def log(options)
43
+ log_info(options)
44
+ log_warn(options)
45
+ log_json(options)
46
+ end
47
+
48
+ # rubocop:disable Metrics/AbcSize
49
+ def log_info(o)
50
+ return if logger.nil?
51
+ logger.info "[EbayRequest] | Url | #{o[:url]}"
52
+ logger.info "[EbayRequest] | Headers | #{o[:headers]}"
53
+ logger.info "[EbayRequest] | Body | #{o[:request_payload]}"
54
+ logger.info "[EbayRequest] | Response | #{fix_utf(o[:response_payload])}"
55
+ logger.info "[EbayRequest] | Time | #{o[:time]} #{o[:callname]}"
56
+ end
57
+ # rubocop:enable Metrics/AbcSize
58
+
59
+ def log_warn(o)
60
+ return if warn_logger.nil? || o[:warnings].empty?
61
+ warn_logger.warn(
62
+ "[EbayRequest] | #{o[:callname]} | #{o[:warnings].inspect}"
63
+ )
64
+ end
65
+
66
+ def log_json(options)
67
+ return if json_logger.nil?
68
+ json_logger.info(options)
69
+ end
70
+
71
+ def fix_utf(response)
72
+ response.encode(
73
+ "UTF-8", undef: :replace, invalid: :replace, replace: " "
74
+ )
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+ class EbayRequest::Auth < EbayRequest::Trading
3
+ def session_id
4
+ response!("GetSessionID", RuName: config.runame)
5
+ end
6
+
7
+ def token(session_id)
8
+ response!("FetchToken", SessionID: session_id)
9
+ end
10
+
11
+ def user(auth_token)
12
+ response!("GetUser", RequesterCredentials: { eBayAuthToken: auth_token },
13
+ DetailLevel: "ReturnAll")
14
+ end
15
+
16
+ def ebay_login_url(session_id, ruparams = {})
17
+ params = [
18
+ "SignIn",
19
+ "RuName=#{CGI.escape(config.runame)}",
20
+ "SessID=#{CGI.escape(session_id)}",
21
+ ]
22
+ ruparams = CGI.escape(ruparams.map { |k, v| "#{k}=#{v}" }.join("&"))
23
+ params << "ruparams=#{CGI.escape(ruparams)}"
24
+
25
+ "#{signin_endpoint}?#{params.join('&')}"
26
+ end
27
+
28
+ private
29
+
30
+ def signin_endpoint
31
+ URI.parse(
32
+ with_sandbox("https://signin%{sandbox}.ebay.com/ws/eBayISAPI.dll")
33
+ )
34
+ end
35
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+ class EbayRequest::Base
3
+ def initialize(options = {})
4
+ @options = options
5
+ end
6
+
7
+ attr_reader :options
8
+
9
+ def config
10
+ @config ||= EbayRequest.config(options[:env])
11
+ end
12
+
13
+ def siteid
14
+ @siteid ||=
15
+ options[:siteid] ||
16
+ EbayRequest::Config.site_id_from_globalid(options[:globalid]) ||
17
+ 0
18
+ end
19
+
20
+ def globalid
21
+ @globalid ||=
22
+ options[:globalid] ||
23
+ EbayRequest::Config.globalid_from_site_id(options[:siteid]) ||
24
+ "EBAY-US"
25
+ end
26
+
27
+ def response(callname, payload)
28
+ config.validate!
29
+ request(URI.parse(with_sandbox(endpoint)), callname, payload)
30
+ end
31
+
32
+ def response!(callname, payload)
33
+ response(callname, payload).data!
34
+ end
35
+
36
+ private
37
+
38
+ def endpoint
39
+ raise NotImplementedError, "Implement #{self.class.name}#endpoint"
40
+ end
41
+
42
+ def headers(_callname)
43
+ @options[:headers] || {}
44
+ end
45
+
46
+ def payload(_callname, _request)
47
+ raise NotImplementedError, "Implement #{self.class.name}#payload"
48
+ end
49
+
50
+ def parse(response)
51
+ MultiXml.parse(response)
52
+ end
53
+
54
+ def process(response, callname)
55
+ data = response["#{callname}Response"]
56
+
57
+ raise EbayRequest::Error, "#{callname} response is blank" if data.nil?
58
+ EbayRequest::Response.new(
59
+ callname, data, errors_for(data), self.class::FATAL_ERRORS
60
+ )
61
+ end
62
+
63
+ def with_sandbox(value)
64
+ # rubocop:disable Style/FormatString
65
+ value % { sandbox: config.sandbox? ? ".sandbox" : "" }
66
+ end
67
+
68
+ # rubocop:disable Metrics/MethodLength
69
+ def request(url, callname, request)
70
+ h = headers(callname)
71
+ b = payload(callname, request)
72
+
73
+ post = Net::HTTP::Post.new(url.path, h)
74
+ post.body = b
75
+
76
+ response, time = make_request(url, post)
77
+
78
+ response_object = process(parse(response), callname)
79
+ ensure
80
+ EbayRequest.log(
81
+ url: url.to_s,
82
+ callname: callname,
83
+ headers: h,
84
+ request_payload: b,
85
+ response_payload: response,
86
+ time: time,
87
+ warnings: response_object&.warnings,
88
+ errors: response_object&.errors,
89
+ success: response_object&.success?
90
+ )
91
+ end
92
+ # rubocop:enable Metrics/MethodLength
93
+
94
+ def make_request(url, post)
95
+ start_time = Time.now
96
+ http = prepare(url)
97
+ response = http.start { |r| r.request(post) }.body
98
+ [response, Time.now - start_time]
99
+ end
100
+
101
+ def prepare(url)
102
+ Net::HTTP.new(url.host, url.port).tap do |http|
103
+ http.read_timeout = config.timeout
104
+
105
+ if url.port == 443
106
+ http.use_ssl = true
107
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
108
+ end
109
+ end
110
+ end
111
+
112
+ def errors_for(_r)
113
+ raise NotImplementedError, "Implement #{self.class.name}#errors_for"
114
+ end
115
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ class EbayRequest::BusinessPolicies < EbayRequest::Base
3
+ private
4
+
5
+ SERVICE_NAME = "SellerProfilesManagementService"
6
+
7
+ def payload(callname, request)
8
+ key_converter = ->(key) { key.camelize(:lower) }
9
+ request = Gyoku.xml(request, key_converter: key_converter)
10
+
11
+ %(<?xml version="1.0" encoding="utf-8"?>\
12
+ <#{callname}Request xmlns="http://www.ebay.com/marketplace/selling">\
13
+ #{request}</#{callname}Request>)
14
+ end
15
+
16
+ def endpoint
17
+ "https://svcs%{sandbox}.ebay.com/services/selling/v1/#{SERVICE_NAME}"
18
+ end
19
+
20
+ def headers(callname)
21
+ super.merge(
22
+ "Content-Type" => "text/xml",
23
+ "X-EBAY-SOA-SECURITY-TOKEN" => options[:token].to_s,
24
+ "X-EBAY-SOA-SERVICE-NAME" => SERVICE_NAME,
25
+ "X-EBAY-SOA-OPERATION-NAME" => callname,
26
+ "X-EBAY-SOA-CONTENT-TYPE" => "XML",
27
+ "X-EBAY-SOA-GLOBAL-ID" => siteid.to_s
28
+ )
29
+ end
30
+
31
+ def errors_for(r)
32
+ [r.dig("errorMessage", "error")]
33
+ .flatten
34
+ .compact
35
+ .map { |e| [e["severity"], e["errorId"], e["message"]] }
36
+ end
37
+
38
+ FATAL_ERRORS = {}.freeze
39
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+ class EbayRequest::Config
3
+ attr_accessor :appid
4
+ attr_accessor :certid
5
+ attr_accessor :devid
6
+ attr_accessor :runame
7
+
8
+ attr_accessor :sandbox
9
+ attr_accessor :version
10
+ attr_accessor :timeout
11
+
12
+ alias sandbox? sandbox
13
+
14
+ def initialize
15
+ @sandbox ||= true
16
+ @version ||= 941
17
+ @timeout ||= 60
18
+ end
19
+
20
+ def validate!
21
+ %w(appid certid devid runame).each do |attr|
22
+ value = public_send(attr)
23
+ raise "Set EbayRequest.config.#{attr}" if value.nil? || value.empty?
24
+ end
25
+ end
26
+
27
+ class << self
28
+ def globalid_from_site_id(site_id)
29
+ (site = sites_by_id[site_id]) && site.globalid
30
+ end
31
+
32
+ def site_id_from_globalid(globalid)
33
+ (site = sites_by_globalid[globalid.to_s.upcase]) && site.id
34
+ end
35
+
36
+ def site_id_from_name(name)
37
+ (site = sites_by_name[name]) && site.id
38
+ end
39
+
40
+ def sites
41
+ @sites ||=
42
+ YAML.load_file(
43
+ File.join(File.dirname(__FILE__), "../../config/sites.yml")
44
+ ).map { |h| EbayRequest::Site.new h }
45
+ end
46
+
47
+ def sites_by_id
48
+ @sites_by_id ||= Hash[sites.map { |s| [s.id, s] }]
49
+ end
50
+
51
+ def sites_by_globalid
52
+ @sites_by_globalid ||= Hash[sites.map { |s| [s.globalid, s] }]
53
+ end
54
+
55
+ def sites_by_name
56
+ @sites_by_name ||= Hash[sites.map { |s| [s.name, s] }]
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ class EbayRequest::Error < StandardError
3
+ def initialize(msg = "EbayRequest error", errors = {})
4
+ super(msg)
5
+ @errors = errors
6
+ end
7
+
8
+ attr_reader :errors
9
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ class EbayRequest::Finding < EbayRequest::Base
3
+ private
4
+
5
+ def payload(callname, request)
6
+ request = Gyoku.xml(request)
7
+
8
+ %(<?xml version="1.0" encoding="utf-8"?><#{callname}Request\
9
+ xmlns="http://www.ebay.com/marketplace/search/v1/services">\
10
+ #{request}</#{callname}Request>)
11
+ end
12
+
13
+ def endpoint
14
+ "http://svcs%{sandbox}.ebay.com/services/search/FindingService/v1"
15
+ end
16
+
17
+ def headers(callname)
18
+ super.merge(
19
+ "X-EBAY-SOA-SERVICE-NAME" => "FindingService",
20
+ "X-EBAY-SOA-SERVICE-VERSION" => "1.9.0",
21
+ "X-EBAY-SOA-SECURITY-APPNAME" => config.appid,
22
+ "X-EBAY-SOA-OPERATION-NAME" => callname,
23
+ "X-EBAY-SOA-REQUEST-DATA-FORMAT" => "XML",
24
+ "X-EBAY-SOA-GLOBAL-ID" => globalid.to_s
25
+ )
26
+ end
27
+
28
+ def errors_for(r)
29
+ [r.dig("errorMessage", "error")]
30
+ .flatten
31
+ .compact
32
+ .map { |e| [e["severity"], e["errorId"], e["message"]] }
33
+ end
34
+
35
+ FATAL_ERRORS = {}.freeze
36
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ class EbayRequest::Response
3
+ extend Dry::Initializer
4
+
5
+ param :callname
6
+ param :data
7
+ param :errors_data
8
+ param :fatal_errors
9
+
10
+ def success?
11
+ ack = data["ack"] || data["Ack"]
12
+ %w(Success Warning).include?(ack)
13
+ end
14
+
15
+ def data!
16
+ raise error unless success?
17
+ data
18
+ end
19
+
20
+ def errors
21
+ severity("Error")
22
+ end
23
+
24
+ def warnings
25
+ severity("Warning")
26
+ end
27
+
28
+ def severity(severity)
29
+ Hash[errors_data.map { |s, c, m| [c.to_i, m] if s == severity }.compact]
30
+ end
31
+
32
+ def error_class
33
+ fatal_code = (errors.keys.map(&:to_i) & fatal_errors.keys).first
34
+ fatal_errors[fatal_code] || EbayRequest::Error
35
+ end
36
+
37
+ def error
38
+ error_class.new(errors.values.join(", "), errors)
39
+ end
40
+ end