ebay_request 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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