ebay-api-ruby 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 282125c3f9f1addb39262ff57f5d817264dee5d8e39e00b22ede983299424ae5
4
+ data.tar.gz: 8cfabcedcccfd2ee3c83a1fb0437e2059119b11676249df6c233b972f2ea596d
5
+ SHA512:
6
+ metadata.gz: c1a0b61cbaa0cba58e3b2abc036f7e378f46663a8daaf528ec730a42198bd675ccd265a26482993cd868b2d16aa1d0507a9a13d3c89eb408df52ae10c18fb6d9
7
+ data.tar.gz: 516284188e67e58bdbb7b6ab430d6903bac9c2075398a061a1a8342d9075d1d9d1aa5a7892f1b59a568891ff3401c32be0497b518250f68f4ba7fc6b865b6e46
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.1.0] - 2026-03-26
6
+
7
+ ### Added
8
+
9
+ - Initial release
10
+ - OAuth 2.0 Client Credentials authentication with token caching
11
+ - Browse API: search, get_item, get_items_by_item_group, search_by_image
12
+ - Finding API: findItemsByKeywords, findCompletedItems, findItemsAdvanced, findItemsByCategory
13
+ - Taxonomy API: get_default_category_tree_id, get_category_tree, get_category_subtree
14
+ - Sandbox mode support
15
+ - Full RSpec test suite with WebMock
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eduardo Souza
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # ebay-api-ruby
2
+
3
+ A modern Ruby gem for the eBay REST APIs. Supports the Browse API, Finding API, and Taxonomy API with OAuth 2.0 authentication.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'ebay-api-ruby'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ Or install it yourself:
20
+
21
+ ```bash
22
+ gem install ebay-api-ruby
23
+ ```
24
+
25
+ ## Configuration
26
+
27
+ ```ruby
28
+ require 'ebay'
29
+
30
+ Ebay.configure do |config|
31
+ config.client_id = ENV['EBAY_CLIENT_ID']
32
+ config.client_secret = ENV['EBAY_CLIENT_SECRET']
33
+ config.app_id = ENV['EBAY_APP_ID'] # Optional, defaults to client_id for Finding API
34
+ config.marketplace_id = 'EBAY_US' # Default
35
+ config.timeout = 30 # Default
36
+ end
37
+ ```
38
+
39
+ ### Sandbox Mode
40
+
41
+ ```ruby
42
+ Ebay.configure do |config|
43
+ config.client_id = ENV['EBAY_SANDBOX_CLIENT_ID']
44
+ config.client_secret = ENV['EBAY_SANDBOX_CLIENT_SECRET']
45
+ config.sandbox!
46
+ end
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ ### Browse API
52
+
53
+ Search for items, get item details, and search by image.
54
+
55
+ ```ruby
56
+ browse = Ebay::Browse.new
57
+
58
+ # Search items
59
+ results = browse.search(q: "vintage rolex", limit: 10)
60
+ results['itemSummaries'].each do |item|
61
+ puts "#{item['title']} - #{item['price']['value']} #{item['price']['currency']}"
62
+ end
63
+
64
+ # Get a specific item
65
+ item = browse.get_item("v1|123456789|0")
66
+
67
+ # Get items by group
68
+ items = browse.get_items_by_item_group("group123")
69
+
70
+ # Search by image
71
+ results = browse.search_by_image(Base64.encode64(File.read("watch.jpg")))
72
+ ```
73
+
74
+ ### Finding API
75
+
76
+ Search active and completed/sold listings. Uses the legacy Finding Service (no OAuth required, just app_id).
77
+
78
+ ```ruby
79
+ finding = Ebay::Finding.new
80
+
81
+ # Search by keywords
82
+ results = finding.find_items_by_keywords("macbook pro m3")
83
+
84
+ # Find completed/sold items (great for price history)
85
+ sold = finding.find_completed_items("pokemon base set charizard")
86
+
87
+ # Advanced search with filters
88
+ results = finding.find_items_advanced(keywords: "nike jordan", categoryId: "93427")
89
+
90
+ # Search by category
91
+ results = finding.find_items_by_category("9355")
92
+ ```
93
+
94
+ ### Taxonomy API
95
+
96
+ Get category trees and browse category hierarchies.
97
+
98
+ ```ruby
99
+ taxonomy = Ebay::Taxonomy.new
100
+
101
+ # Get default category tree ID for a marketplace
102
+ tree_info = taxonomy.get_default_category_tree_id("EBAY_US")
103
+ tree_id = tree_info['categoryTreeId']
104
+
105
+ # Get the full category tree
106
+ tree = taxonomy.get_category_tree(tree_id)
107
+
108
+ # Get a subtree
109
+ subtree = taxonomy.get_category_subtree(tree_id, "9355")
110
+ ```
111
+
112
+ ### Authentication
113
+
114
+ The gem handles OAuth 2.0 Client Credentials authentication automatically. Tokens are cached and refreshed when expired.
115
+
116
+ ```ruby
117
+ client = Ebay.client
118
+
119
+ # Manual authentication (usually not needed)
120
+ client.authenticate!
121
+
122
+ # Direct API calls
123
+ result = client.get("/buy/browse/v1/item_summary/search", q: "test")
124
+ result = client.post("/some/endpoint", { data: "value" })
125
+ ```
126
+
127
+ ## Error Handling
128
+
129
+ ```ruby
130
+ begin
131
+ browse.search(q: "test")
132
+ rescue Ebay::AuthenticationError => e
133
+ puts "Auth failed: #{e.message}"
134
+ rescue Ebay::NotFoundError => e
135
+ puts "Not found: #{e.message} (#{e.status_code})"
136
+ rescue Ebay::RateLimitError => e
137
+ puts "Rate limited. Retry after: #{e.retry_after}"
138
+ rescue Ebay::ValidationError => e
139
+ puts "Validation error: #{e.errors}"
140
+ rescue Ebay::APIError => e
141
+ puts "API error: #{e.message} (#{e.status_code})"
142
+ rescue Ebay::ConfigurationError => e
143
+ puts "Config error: #{e.message}"
144
+ end
145
+ ```
146
+
147
+ ## Development
148
+
149
+ ```bash
150
+ git clone https://github.com/esouza/ebay-api-ruby.git
151
+ cd ebay-api-ruby
152
+ bundle install
153
+ bundle exec rspec
154
+ ```
155
+
156
+ ## Contributing
157
+
158
+ Bug reports and pull requests are welcome on GitHub at https://github.com/esouza/ebay-api-ruby. See [CONTRIBUTING.md](CONTRIBUTING.md).
159
+
160
+ ## License
161
+
162
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
data/lib/ebay/base.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ebay
4
+ class Base
5
+ attr_reader :client
6
+
7
+ def initialize(client = nil)
8
+ @client = client || Ebay.client
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Ebay
6
+ class Browse < Base
7
+ def search(q:, **params)
8
+ client.get("/buy/browse/v1/item_summary/search", { q: q }.merge(params))
9
+ end
10
+
11
+ def get_item(item_id)
12
+ client.get("/buy/browse/v1/item/#{URI.encode_www_form_component(item_id)}")
13
+ end
14
+
15
+ def get_items_by_item_group(item_group_id)
16
+ client.get("/buy/browse/v1/item/get_items_by_item_group", item_group_id: item_group_id)
17
+ end
18
+
19
+ def search_by_image(image_base64, **params)
20
+ client.post("/buy/browse/v1/item_summary/search_by_image", { image: image_base64 }.merge(params))
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ebay
4
+ class Client
5
+ include HTTParty
6
+
7
+ attr_reader :configuration
8
+
9
+ def initialize(configuration = nil)
10
+ @configuration = configuration || Ebay.configuration
11
+ @access_token = nil
12
+ @token_expires_at = nil
13
+ validate_configuration!
14
+ end
15
+
16
+ def authenticate!
17
+ credentials = Base64.strict_encode64("#{configuration.client_id}:#{configuration.client_secret}")
18
+
19
+ response = self.class.post(
20
+ "#{configuration.base_url}/identity/v1/oauth2/token",
21
+ headers: {
22
+ 'Authorization' => "Basic #{credentials}",
23
+ 'Content-Type' => 'application/x-www-form-urlencoded'
24
+ },
25
+ body: "grant_type=client_credentials&scope=https://api.ebay.com/oauth/api_scope",
26
+ timeout: configuration.timeout
27
+ )
28
+
29
+ case response.code
30
+ when 200
31
+ data = response.parsed_response
32
+ @access_token = data['access_token']
33
+ @token_expires_at = Time.now + (data['expires_in'] || 7200).to_i - 60
34
+ @access_token
35
+ else
36
+ raise AuthenticationError, "OAuth token request failed: #{response.body}"
37
+ end
38
+ end
39
+
40
+ def get(path, params = {})
41
+ request(:get, path, query: params)
42
+ end
43
+
44
+ def post(path, data = {})
45
+ request(:post, path, body: data.to_json)
46
+ end
47
+
48
+ private
49
+
50
+ def validate_configuration!
51
+ raise ConfigurationError unless configuration&.valid?
52
+ end
53
+
54
+ def ensure_authenticated!
55
+ if @access_token.nil? || @token_expires_at.nil? || Time.now >= @token_expires_at
56
+ authenticate!
57
+ end
58
+ end
59
+
60
+ def request(method, path, options = {})
61
+ ensure_authenticated!
62
+
63
+ url = "#{configuration.base_url}#{path}"
64
+ request_options = {
65
+ headers: default_headers,
66
+ timeout: configuration.timeout
67
+ }
68
+
69
+ if options[:query] && !options[:query].empty?
70
+ request_options[:query] = options[:query]
71
+ end
72
+
73
+ if options[:body]
74
+ request_options[:body] = options[:body]
75
+ end
76
+
77
+ response = self.class.send(method, url, request_options)
78
+ handle_response(response)
79
+ end
80
+
81
+ def default_headers
82
+ {
83
+ 'Authorization' => "Bearer #{@access_token}",
84
+ 'Content-Type' => 'application/json',
85
+ 'Accept' => 'application/json'
86
+ }
87
+ end
88
+
89
+ def handle_response(response)
90
+ case response.code
91
+ when 200, 201, 202, 204
92
+ response.parsed_response
93
+ when 401
94
+ raise AuthenticationError, "Authentication failed: #{response.body}"
95
+ when 404
96
+ raise NotFoundError.new("Resource not found", response.code, response.body)
97
+ when 429
98
+ retry_after = response.headers['retry-after']
99
+ raise RateLimitError.new("Rate limit exceeded", response.code, response.body, retry_after)
100
+ when 400, 422
101
+ parsed = response.parsed_response || {}
102
+ errors = parsed['errors'] || parsed['message'] || {}
103
+ raise ValidationError.new(
104
+ parsed['message'] || 'Validation failed',
105
+ response.code,
106
+ response.body,
107
+ errors
108
+ )
109
+ when 500..599
110
+ raise APIError.new("Server error", response.code, response.body)
111
+ else
112
+ raise APIError.new("Unexpected response", response.code, response.body)
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ebay
4
+ class Configuration
5
+ attr_accessor :client_id, :client_secret, :app_id,
6
+ :base_url, :sandbox, :timeout, :marketplace_id
7
+
8
+ def initialize
9
+ @base_url = "https://api.ebay.com"
10
+ @sandbox = false
11
+ @timeout = 30
12
+ @marketplace_id = "EBAY_US"
13
+ end
14
+
15
+ def sandbox!
16
+ @sandbox = true
17
+ @base_url = "https://api.sandbox.ebay.com"
18
+ self
19
+ end
20
+
21
+ def valid?
22
+ !client_id.nil? && !client_id.empty? &&
23
+ !client_secret.nil? && !client_secret.empty?
24
+ end
25
+
26
+ def finding_url
27
+ sandbox ? "https://svcs.sandbox.ebay.com/services/search/FindingService/v1" : "https://svcs.ebay.com/services/search/FindingService/v1"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ebay
4
+ class Error < StandardError; end
5
+
6
+ class ConfigurationError < Error
7
+ def initialize(message = "Missing required configuration: client_id and client_secret")
8
+ super(message)
9
+ end
10
+ end
11
+
12
+ class AuthenticationError < Error
13
+ def initialize(message = "Authentication failed")
14
+ super(message)
15
+ end
16
+ end
17
+
18
+ class APIError < Error
19
+ attr_reader :status_code, :response_body
20
+
21
+ def initialize(message, status_code = nil, response_body = nil)
22
+ super(message)
23
+ @status_code = status_code
24
+ @response_body = response_body
25
+ end
26
+ end
27
+
28
+ class ValidationError < APIError
29
+ attr_reader :errors
30
+
31
+ def initialize(message, status_code = nil, response_body = nil, errors = {})
32
+ super(message, status_code, response_body)
33
+ @errors = errors
34
+ end
35
+ end
36
+
37
+ class NotFoundError < APIError
38
+ def initialize(message = "Resource not found", status_code = 404, response_body = nil)
39
+ super(message, status_code, response_body)
40
+ end
41
+ end
42
+
43
+ class RateLimitError < APIError
44
+ attr_reader :retry_after
45
+
46
+ def initialize(message = "Rate limit exceeded", status_code = 429, response_body = nil, retry_after = nil)
47
+ super(message, status_code, response_body)
48
+ @retry_after = retry_after
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ebay
4
+ class Finding < Base
5
+ def find_items_by_keywords(keywords, **params)
6
+ finding_request("findItemsByKeywords", { keywords: keywords }.merge(params))
7
+ end
8
+
9
+ def find_completed_items(keywords, **params)
10
+ finding_request("findCompletedItems", { keywords: keywords }.merge(params))
11
+ end
12
+
13
+ def find_items_advanced(**params)
14
+ finding_request("findItemsAdvanced", params)
15
+ end
16
+
17
+ def find_items_by_category(category_id, **params)
18
+ finding_request("findItemsByCategory", { categoryId: category_id }.merge(params))
19
+ end
20
+
21
+ private
22
+
23
+ def finding_request(operation, params)
24
+ url = client.configuration.finding_url
25
+ query = {
26
+ "OPERATION-NAME" => operation,
27
+ "SERVICE-VERSION" => "1.0.0",
28
+ "SECURITY-APPNAME" => client.configuration.app_id || client.configuration.client_id,
29
+ "RESPONSE-DATA-FORMAT" => "JSON",
30
+ "REST-PAYLOAD" => ""
31
+ }.merge(params)
32
+
33
+ response = HTTParty.get(url, query: query, timeout: client.configuration.timeout)
34
+ handle_finding_response(response, operation)
35
+ end
36
+
37
+ def handle_finding_response(response, operation)
38
+ return response.parsed_response unless response.code >= 400
39
+ raise APIError.new("Finding API error", response.code, response.body)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ebay
4
+ class Taxonomy < Base
5
+ def get_default_category_tree_id(marketplace_id = nil)
6
+ mid = marketplace_id || client.configuration.marketplace_id
7
+ client.get("/commerce/taxonomy/v1/get_default_category_tree_id", marketplace_id: mid)
8
+ end
9
+
10
+ def get_category_tree(category_tree_id)
11
+ client.get("/commerce/taxonomy/v1/category_tree/#{category_tree_id}")
12
+ end
13
+
14
+ def get_category_subtree(category_tree_id, category_id)
15
+ client.get("/commerce/taxonomy/v1/category_tree/#{category_tree_id}/get_category_subtree", category_id: category_id)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ebay
4
+ VERSION = "0.1.0"
5
+ end
data/lib/ebay.rb ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+ require 'json'
5
+ require 'base64'
6
+
7
+ require_relative 'ebay/version'
8
+ require_relative 'ebay/errors'
9
+ require_relative 'ebay/configuration'
10
+ require_relative 'ebay/client'
11
+ require_relative 'ebay/base'
12
+ require_relative 'ebay/browse'
13
+ require_relative 'ebay/finding'
14
+ require_relative 'ebay/taxonomy'
15
+
16
+ module Ebay
17
+ class << self
18
+ attr_accessor :configuration
19
+
20
+ def configure
21
+ self.configuration ||= Configuration.new
22
+ yield(configuration) if block_given?
23
+ end
24
+
25
+ def client
26
+ @client ||= Client.new(configuration)
27
+ end
28
+
29
+ def reset!
30
+ @client = nil
31
+ @configuration = nil
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Ebay::Browse do
4
+ let(:client) { Ebay.client }
5
+ let(:browse) { described_class.new(client) }
6
+
7
+ before { stub_oauth_token }
8
+
9
+ describe "#search" do
10
+ it "searches items by keyword" do
11
+ stub_request(:get, "https://api.ebay.com/buy/browse/v1/item_summary/search")
12
+ .with(query: hash_including(q: "iphone"))
13
+ .to_return(status: 200, body: '{"itemSummaries": []}', headers: { 'Content-Type' => 'application/json' })
14
+
15
+ result = browse.search(q: "iphone")
16
+ expect(result).to eq("itemSummaries" => [])
17
+ end
18
+
19
+ it "passes additional params" do
20
+ stub_request(:get, "https://api.ebay.com/buy/browse/v1/item_summary/search")
21
+ .with(query: hash_including(q: "iphone", limit: "10"))
22
+ .to_return(status: 200, body: '{"itemSummaries": []}', headers: { 'Content-Type' => 'application/json' })
23
+
24
+ browse.search(q: "iphone", limit: "10")
25
+ expect(WebMock).to have_requested(:get, "https://api.ebay.com/buy/browse/v1/item_summary/search")
26
+ .with(query: hash_including(q: "iphone", limit: "10"))
27
+ end
28
+ end
29
+
30
+ describe "#get_item" do
31
+ it "retrieves a single item" do
32
+ stub_request(:get, "https://api.ebay.com/buy/browse/v1/item/v1%7C123%7C0")
33
+ .to_return(status: 200, body: '{"itemId": "v1|123|0"}', headers: { 'Content-Type' => 'application/json' })
34
+
35
+ result = browse.get_item("v1|123|0")
36
+ expect(result).to eq("itemId" => "v1|123|0")
37
+ end
38
+ end
39
+
40
+ describe "#get_items_by_item_group" do
41
+ it "retrieves items by group" do
42
+ stub_request(:get, "https://api.ebay.com/buy/browse/v1/item/get_items_by_item_group")
43
+ .with(query: { item_group_id: "group123" })
44
+ .to_return(status: 200, body: '{"items": []}', headers: { 'Content-Type' => 'application/json' })
45
+
46
+ result = browse.get_items_by_item_group("group123")
47
+ expect(result).to eq("items" => [])
48
+ end
49
+ end
50
+
51
+ describe "#search_by_image" do
52
+ it "searches by image" do
53
+ stub_request(:post, "https://api.ebay.com/buy/browse/v1/item_summary/search_by_image")
54
+ .to_return(status: 200, body: '{"itemSummaries": []}', headers: { 'Content-Type' => 'application/json' })
55
+
56
+ result = browse.search_by_image("base64data")
57
+ expect(result).to eq("itemSummaries" => [])
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Ebay::Client do
4
+ let(:client) { Ebay.client }
5
+
6
+ describe "#initialize" do
7
+ it "raises ConfigurationError without valid config" do
8
+ Ebay.reset!
9
+ Ebay.configure { |c| c.client_id = nil }
10
+ expect { Ebay.client }.to raise_error(Ebay::ConfigurationError)
11
+ end
12
+ end
13
+
14
+ describe "#authenticate!" do
15
+ it "obtains an access token" do
16
+ stub_oauth_token
17
+ token = client.authenticate!
18
+ expect(token).to eq("test_token")
19
+ end
20
+
21
+ it "sends correct authorization header" do
22
+ stub_oauth_token
23
+ client.authenticate!
24
+
25
+ expected_credentials = Base64.strict_encode64("test_client_id:test_client_secret")
26
+ expect(WebMock).to have_requested(:post, "https://api.ebay.com/identity/v1/oauth2/token")
27
+ .with(headers: { 'Authorization' => "Basic #{expected_credentials}" })
28
+ end
29
+
30
+ it "raises AuthenticationError on failure" do
31
+ stub_request(:post, "https://api.ebay.com/identity/v1/oauth2/token")
32
+ .to_return(status: 401, body: "Unauthorized")
33
+
34
+ expect { client.authenticate! }.to raise_error(Ebay::AuthenticationError)
35
+ end
36
+ end
37
+
38
+ describe "#get" do
39
+ before { stub_oauth_token }
40
+
41
+ it "makes authenticated GET requests" do
42
+ stub_request(:get, "https://api.ebay.com/test")
43
+ .to_return(status: 200, body: '{"result": "ok"}', headers: { 'Content-Type' => 'application/json' })
44
+
45
+ result = client.get("/test")
46
+ expect(result).to eq("result" => "ok")
47
+ end
48
+
49
+ it "passes query params" do
50
+ stub_request(:get, "https://api.ebay.com/test?q=shoes")
51
+ .to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
52
+
53
+ client.get("/test", q: "shoes")
54
+ expect(WebMock).to have_requested(:get, "https://api.ebay.com/test").with(query: { q: "shoes" })
55
+ end
56
+
57
+ it "auto-authenticates before request" do
58
+ stub_request(:get, "https://api.ebay.com/test")
59
+ .to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
60
+
61
+ client.get("/test")
62
+ expect(WebMock).to have_requested(:post, "https://api.ebay.com/identity/v1/oauth2/token").once
63
+ end
64
+ end
65
+
66
+ describe "#post" do
67
+ before { stub_oauth_token }
68
+
69
+ it "makes authenticated POST requests" do
70
+ stub_request(:post, "https://api.ebay.com/test")
71
+ .to_return(status: 200, body: '{"created": true}', headers: { 'Content-Type' => 'application/json' })
72
+
73
+ result = client.post("/test", { name: "item" })
74
+ expect(result).to eq("created" => true)
75
+ end
76
+ end
77
+
78
+ describe "error handling" do
79
+ before { stub_oauth_token }
80
+
81
+ it "raises NotFoundError on 404" do
82
+ stub_request(:get, "https://api.ebay.com/test")
83
+ .to_return(status: 404, body: "Not Found")
84
+
85
+ expect { client.get("/test") }.to raise_error(Ebay::NotFoundError)
86
+ end
87
+
88
+ it "raises RateLimitError on 429" do
89
+ stub_request(:get, "https://api.ebay.com/test")
90
+ .to_return(status: 429, body: "Too Many Requests", headers: { 'retry-after' => '60' })
91
+
92
+ expect { client.get("/test") }.to raise_error(Ebay::RateLimitError) do |error|
93
+ expect(error.retry_after).to eq('60')
94
+ end
95
+ end
96
+
97
+ it "raises ValidationError on 400" do
98
+ stub_request(:get, "https://api.ebay.com/test")
99
+ .to_return(status: 400, body: '{"message": "Bad Request"}', headers: { 'Content-Type' => 'application/json' })
100
+
101
+ expect { client.get("/test") }.to raise_error(Ebay::ValidationError)
102
+ end
103
+
104
+ it "raises APIError on 500" do
105
+ stub_request(:get, "https://api.ebay.com/test")
106
+ .to_return(status: 500, body: "Internal Server Error")
107
+
108
+ expect { client.get("/test") }.to raise_error(Ebay::APIError)
109
+ end
110
+ end
111
+
112
+ describe "token caching" do
113
+ it "reuses token for multiple requests" do
114
+ stub_oauth_token
115
+
116
+ stub_request(:get, "https://api.ebay.com/test")
117
+ .to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
118
+
119
+ client.get("/test")
120
+ client.get("/test")
121
+
122
+ expect(WebMock).to have_requested(:post, "https://api.ebay.com/identity/v1/oauth2/token").once
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Ebay::Configuration do
4
+ subject(:config) { described_class.new }
5
+
6
+ describe "#initialize" do
7
+ it "sets default base_url" do
8
+ expect(config.base_url).to eq("https://api.ebay.com")
9
+ end
10
+
11
+ it "sets default sandbox to false" do
12
+ expect(config.sandbox).to be false
13
+ end
14
+
15
+ it "sets default timeout to 30" do
16
+ expect(config.timeout).to eq(30)
17
+ end
18
+
19
+ it "sets default marketplace_id" do
20
+ expect(config.marketplace_id).to eq("EBAY_US")
21
+ end
22
+ end
23
+
24
+ describe "#sandbox!" do
25
+ it "enables sandbox mode" do
26
+ config.sandbox!
27
+ expect(config.sandbox).to be true
28
+ end
29
+
30
+ it "changes base_url to sandbox" do
31
+ config.sandbox!
32
+ expect(config.base_url).to eq("https://api.sandbox.ebay.com")
33
+ end
34
+
35
+ it "returns self for chaining" do
36
+ expect(config.sandbox!).to eq(config)
37
+ end
38
+ end
39
+
40
+ describe "#valid?" do
41
+ it "returns false when client_id is nil" do
42
+ config.client_secret = "secret"
43
+ expect(config.valid?).to be false
44
+ end
45
+
46
+ it "returns false when client_secret is nil" do
47
+ config.client_id = "id"
48
+ expect(config.valid?).to be false
49
+ end
50
+
51
+ it "returns false when client_id is empty" do
52
+ config.client_id = ""
53
+ config.client_secret = "secret"
54
+ expect(config.valid?).to be false
55
+ end
56
+
57
+ it "returns true when both are set" do
58
+ config.client_id = "id"
59
+ config.client_secret = "secret"
60
+ expect(config.valid?).to be true
61
+ end
62
+ end
63
+
64
+ describe "#finding_url" do
65
+ it "returns production URL by default" do
66
+ expect(config.finding_url).to eq("https://svcs.ebay.com/services/search/FindingService/v1")
67
+ end
68
+
69
+ it "returns sandbox URL when sandbox enabled" do
70
+ config.sandbox!
71
+ expect(config.finding_url).to eq("https://svcs.sandbox.ebay.com/services/search/FindingService/v1")
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Ebay::Finding do
4
+ let(:client) { Ebay.client }
5
+ let(:finding) { described_class.new(client) }
6
+ let(:finding_url) { "https://svcs.ebay.com/services/search/FindingService/v1" }
7
+
8
+ before do
9
+ stub_oauth_token
10
+ end
11
+
12
+ describe "#find_items_by_keywords" do
13
+ it "searches by keywords" do
14
+ stub_request(:get, finding_url)
15
+ .with(query: hash_including("OPERATION-NAME" => "findItemsByKeywords", "keywords" => "laptop"))
16
+ .to_return(status: 200, body: '{"findItemsByKeywordsResponse": []}', headers: { 'Content-Type' => 'application/json' })
17
+
18
+ result = finding.find_items_by_keywords("laptop")
19
+ expect(result).to eq("findItemsByKeywordsResponse" => [])
20
+ end
21
+
22
+ it "includes correct service params" do
23
+ stub_request(:get, finding_url)
24
+ .with(query: hash_including(
25
+ "OPERATION-NAME" => "findItemsByKeywords",
26
+ "SERVICE-VERSION" => "1.0.0",
27
+ "SECURITY-APPNAME" => "test_app_id",
28
+ "RESPONSE-DATA-FORMAT" => "JSON"
29
+ ))
30
+ .to_return(status: 200, body: '{}', headers: { 'Content-Type' => 'application/json' })
31
+
32
+ finding.find_items_by_keywords("test")
33
+ end
34
+ end
35
+
36
+ describe "#find_completed_items" do
37
+ it "searches completed/sold items" do
38
+ stub_request(:get, finding_url)
39
+ .with(query: hash_including("OPERATION-NAME" => "findCompletedItems", "keywords" => "vintage watch"))
40
+ .to_return(status: 200, body: '{"findCompletedItemsResponse": []}', headers: { 'Content-Type' => 'application/json' })
41
+
42
+ result = finding.find_completed_items("vintage watch")
43
+ expect(result).to eq("findCompletedItemsResponse" => [])
44
+ end
45
+ end
46
+
47
+ describe "#find_items_advanced" do
48
+ it "performs advanced search" do
49
+ stub_request(:get, finding_url)
50
+ .with(query: hash_including("OPERATION-NAME" => "findItemsAdvanced"))
51
+ .to_return(status: 200, body: '{"findItemsAdvancedResponse": []}', headers: { 'Content-Type' => 'application/json' })
52
+
53
+ result = finding.find_items_advanced(keywords: "shoes", categoryId: "3034")
54
+ expect(result).to eq("findItemsAdvancedResponse" => [])
55
+ end
56
+ end
57
+
58
+ describe "#find_items_by_category" do
59
+ it "searches by category" do
60
+ stub_request(:get, finding_url)
61
+ .with(query: hash_including("OPERATION-NAME" => "findItemsByCategory", "categoryId" => "9355"))
62
+ .to_return(status: 200, body: '{"findItemsByCategoryResponse": []}', headers: { 'Content-Type' => 'application/json' })
63
+
64
+ result = finding.find_items_by_category("9355")
65
+ expect(result).to eq("findItemsByCategoryResponse" => [])
66
+ end
67
+ end
68
+
69
+ describe "error handling" do
70
+ it "raises APIError on failure" do
71
+ stub_request(:get, finding_url)
72
+ .with(query: hash_including("OPERATION-NAME" => "findItemsByKeywords"))
73
+ .to_return(status: 500, body: "Server Error")
74
+
75
+ expect { finding.find_items_by_keywords("test") }.to raise_error(Ebay::APIError)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Ebay::Taxonomy do
4
+ let(:client) { Ebay.client }
5
+ let(:taxonomy) { described_class.new(client) }
6
+
7
+ before { stub_oauth_token }
8
+
9
+ describe "#get_default_category_tree_id" do
10
+ it "returns default category tree id" do
11
+ stub_request(:get, "https://api.ebay.com/commerce/taxonomy/v1/get_default_category_tree_id")
12
+ .with(query: { marketplace_id: "EBAY_US" })
13
+ .to_return(status: 200, body: '{"categoryTreeId": "0", "categoryTreeVersion": "119"}', headers: { 'Content-Type' => 'application/json' })
14
+
15
+ result = taxonomy.get_default_category_tree_id
16
+ expect(result).to eq("categoryTreeId" => "0", "categoryTreeVersion" => "119")
17
+ end
18
+
19
+ it "accepts custom marketplace_id" do
20
+ stub_request(:get, "https://api.ebay.com/commerce/taxonomy/v1/get_default_category_tree_id")
21
+ .with(query: { marketplace_id: "EBAY_GB" })
22
+ .to_return(status: 200, body: '{"categoryTreeId": "3"}', headers: { 'Content-Type' => 'application/json' })
23
+
24
+ result = taxonomy.get_default_category_tree_id("EBAY_GB")
25
+ expect(result).to eq("categoryTreeId" => "3")
26
+ end
27
+ end
28
+
29
+ describe "#get_category_tree" do
30
+ it "returns category tree" do
31
+ stub_request(:get, "https://api.ebay.com/commerce/taxonomy/v1/category_tree/0")
32
+ .to_return(status: 200, body: '{"categoryTreeId": "0", "rootCategoryNode": {}}', headers: { 'Content-Type' => 'application/json' })
33
+
34
+ result = taxonomy.get_category_tree("0")
35
+ expect(result).to include("categoryTreeId" => "0")
36
+ end
37
+ end
38
+
39
+ describe "#get_category_subtree" do
40
+ it "returns category subtree" do
41
+ stub_request(:get, "https://api.ebay.com/commerce/taxonomy/v1/category_tree/0/get_category_subtree")
42
+ .with(query: { category_id: "9355" })
43
+ .to_return(status: 200, body: '{"categorySubtreeNode": {}}', headers: { 'Content-Type' => 'application/json' })
44
+
45
+ result = taxonomy.get_category_subtree("0", "9355")
46
+ expect(result).to eq("categorySubtreeNode" => {})
47
+ end
48
+ end
49
+ end
data/spec/examples.txt ADDED
@@ -0,0 +1,43 @@
1
+ example_id | status | run_time |
2
+ ---------------------------------------- | ------ | --------------- |
3
+ ./spec/ebay/browse_spec.rb[1:1:1] | passed | 0.00076 seconds |
4
+ ./spec/ebay/browse_spec.rb[1:1:2] | passed | 0.00193 seconds |
5
+ ./spec/ebay/browse_spec.rb[1:2:1] | passed | 0.00051 seconds |
6
+ ./spec/ebay/browse_spec.rb[1:3:1] | passed | 0.00054 seconds |
7
+ ./spec/ebay/browse_spec.rb[1:4:1] | passed | 0.00048 seconds |
8
+ ./spec/ebay/client_spec.rb[1:1:1] | passed | 0.00004 seconds |
9
+ ./spec/ebay/client_spec.rb[1:2:1] | passed | 0.00022 seconds |
10
+ ./spec/ebay/client_spec.rb[1:2:2] | passed | 0.00031 seconds |
11
+ ./spec/ebay/client_spec.rb[1:2:3] | passed | 0.00026 seconds |
12
+ ./spec/ebay/client_spec.rb[1:3:1] | passed | 0.00041 seconds |
13
+ ./spec/ebay/client_spec.rb[1:3:2] | passed | 0.00065 seconds |
14
+ ./spec/ebay/client_spec.rb[1:3:3] | passed | 0.00039 seconds |
15
+ ./spec/ebay/client_spec.rb[1:4:1] | passed | 0.00065 seconds |
16
+ ./spec/ebay/client_spec.rb[1:5:1] | passed | 0.00044 seconds |
17
+ ./spec/ebay/client_spec.rb[1:5:2] | passed | 0.00052 seconds |
18
+ ./spec/ebay/client_spec.rb[1:5:3] | passed | 0.00059 seconds |
19
+ ./spec/ebay/client_spec.rb[1:5:4] | passed | 0.00047 seconds |
20
+ ./spec/ebay/client_spec.rb[1:6:1] | passed | 0.00057 seconds |
21
+ ./spec/ebay/configuration_spec.rb[1:1:1] | passed | 0.00003 seconds |
22
+ ./spec/ebay/configuration_spec.rb[1:1:2] | passed | 0.00004 seconds |
23
+ ./spec/ebay/configuration_spec.rb[1:1:3] | passed | 0.00003 seconds |
24
+ ./spec/ebay/configuration_spec.rb[1:1:4] | passed | 0.00003 seconds |
25
+ ./spec/ebay/configuration_spec.rb[1:2:1] | passed | 0.00046 seconds |
26
+ ./spec/ebay/configuration_spec.rb[1:2:2] | passed | 0.00005 seconds |
27
+ ./spec/ebay/configuration_spec.rb[1:2:3] | passed | 0.00004 seconds |
28
+ ./spec/ebay/configuration_spec.rb[1:3:1] | passed | 0.00004 seconds |
29
+ ./spec/ebay/configuration_spec.rb[1:3:2] | passed | 0.00003 seconds |
30
+ ./spec/ebay/configuration_spec.rb[1:3:3] | passed | 0.00005 seconds |
31
+ ./spec/ebay/configuration_spec.rb[1:3:4] | passed | 0.00005 seconds |
32
+ ./spec/ebay/configuration_spec.rb[1:4:1] | passed | 0.00003 seconds |
33
+ ./spec/ebay/configuration_spec.rb[1:4:2] | passed | 0.00003 seconds |
34
+ ./spec/ebay/finding_spec.rb[1:1:1] | passed | 0.00134 seconds |
35
+ ./spec/ebay/finding_spec.rb[1:1:2] | passed | 0.02238 seconds |
36
+ ./spec/ebay/finding_spec.rb[1:2:1] | passed | 0.00057 seconds |
37
+ ./spec/ebay/finding_spec.rb[1:3:1] | passed | 0.00077 seconds |
38
+ ./spec/ebay/finding_spec.rb[1:4:1] | passed | 0.00048 seconds |
39
+ ./spec/ebay/finding_spec.rb[1:5:1] | passed | 0.00125 seconds |
40
+ ./spec/ebay/taxonomy_spec.rb[1:1:1] | passed | 0.0007 seconds |
41
+ ./spec/ebay/taxonomy_spec.rb[1:1:2] | passed | 0.00056 seconds |
42
+ ./spec/ebay/taxonomy_spec.rb[1:2:1] | passed | 0.00212 seconds |
43
+ ./spec/ebay/taxonomy_spec.rb[1:3:1] | passed | 0.0006 seconds |
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "ebay"
5
+ require "webmock/rspec"
6
+
7
+ RSpec.configure do |config|
8
+ config.expect_with :rspec do |expectations|
9
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
10
+ end
11
+
12
+ config.mock_with :rspec do |mocks|
13
+ mocks.verify_partial_doubles = true
14
+ end
15
+
16
+ config.shared_context_metadata_behavior = :apply_to_host_groups
17
+ config.filter_run_when_matching :focus
18
+ config.example_status_persistence_file_path = "spec/examples.txt"
19
+ config.disable_monkey_patching!
20
+ config.warnings = true
21
+
22
+ if config.files_to_run.one?
23
+ config.default_formatter = "doc"
24
+ end
25
+
26
+ config.order = :random
27
+ Kernel.srand config.seed
28
+
29
+ config.before(:each) do
30
+ WebMock.disable_net_connect!
31
+ Ebay.reset!
32
+ Ebay.configure do |c|
33
+ c.client_id = "test_client_id"
34
+ c.client_secret = "test_client_secret"
35
+ c.app_id = "test_app_id"
36
+ end
37
+ end
38
+
39
+ config.after(:each) do
40
+ WebMock.reset!
41
+ end
42
+ end
43
+
44
+ def stub_oauth_token
45
+ stub_request(:post, "https://api.ebay.com/identity/v1/oauth2/token")
46
+ .to_return(
47
+ status: 200,
48
+ headers: { 'Content-Type' => 'application/json' },
49
+ body: {
50
+ access_token: "test_token",
51
+ expires_in: 7200,
52
+ token_type: "Application Access Token"
53
+ }.to_json
54
+ )
55
+ end
metadata ADDED
@@ -0,0 +1,192 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ebay-api-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Eduardo Souza
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: base64
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: httparty
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.21'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.21'
41
+ - !ruby/object:Gem::Dependency
42
+ name: json
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.6'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: dotenv
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.8'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.8'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '13.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '13.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.12'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.12'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.50'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.50'
125
+ - !ruby/object:Gem::Dependency
126
+ name: webmock
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.18'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.18'
139
+ description: Ruby gem for integrating with eBay REST APIs. Supports Browse API, Finding
140
+ API, and Taxonomy API with OAuth 2.0 authentication.
141
+ email:
142
+ - eduardo@eduardosouza.com
143
+ executables: []
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - CHANGELOG.md
148
+ - LICENSE.txt
149
+ - README.md
150
+ - lib/ebay.rb
151
+ - lib/ebay/base.rb
152
+ - lib/ebay/browse.rb
153
+ - lib/ebay/client.rb
154
+ - lib/ebay/configuration.rb
155
+ - lib/ebay/errors.rb
156
+ - lib/ebay/finding.rb
157
+ - lib/ebay/taxonomy.rb
158
+ - lib/ebay/version.rb
159
+ - spec/ebay/browse_spec.rb
160
+ - spec/ebay/client_spec.rb
161
+ - spec/ebay/configuration_spec.rb
162
+ - spec/ebay/finding_spec.rb
163
+ - spec/ebay/taxonomy_spec.rb
164
+ - spec/examples.txt
165
+ - spec/spec_helper.rb
166
+ homepage: https://github.com/esouza/ebay-api-ruby
167
+ licenses:
168
+ - MIT
169
+ metadata:
170
+ homepage_uri: https://github.com/esouza/ebay-api-ruby
171
+ source_code_uri: https://github.com/esouza/ebay-api-ruby
172
+ changelog_uri: https://github.com/esouza/ebay-api-ruby/blob/main/CHANGELOG.md
173
+ post_install_message:
174
+ rdoc_options: []
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: 2.7.0
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ requirements: []
188
+ rubygems_version: 3.5.21
189
+ signing_key:
190
+ specification_version: 4
191
+ summary: Ruby gem for the eBay REST APIs
192
+ test_files: []