shopsavvy-sdk 1.0.1 → 1.0.2
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 +4 -4
- data/lib/shopsavvy_data_api/client.rb +49 -27
- data/lib/shopsavvy_data_api/models.rb +221 -47
- data/lib/shopsavvy_data_api/version.rb +1 -1
- data/test_api.rb +94 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d081fbf3392d235e65de90a26888509f78989317334920e88cebe4657befe37c
|
|
4
|
+
data.tar.gz: '028202a7c601c12b53db7854be620abee4f4e3e0479e0ed3f1c1fe2e9107eeac'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c1b1f2fdf1636fb0fa5149dc95e235b90aa5557f3d42c35acf1aff2ebb6ba6f124172178a1b1f9c79ee296e45baf01b69df66f55c876720dbe0063d9c5cc9f2b
|
|
7
|
+
data.tar.gz: da90ddde8a0830d8548e08ccb4f95297940a1418aa7fb66529d46aab50c34f3847cfbe633bc53a99b71635b0e6cd5dcf87c08dbd2750cebbbac11ca686be2f80
|
|
@@ -13,7 +13,7 @@ module ShopsavvyDataApi
|
|
|
13
13
|
# @example Basic usage
|
|
14
14
|
# client = ShopsavvyDataApi::Client.new(api_key: "ss_live_your_api_key_here")
|
|
15
15
|
# product = client.get_product_details("012345678901")
|
|
16
|
-
# puts product.data.
|
|
16
|
+
# puts product.data[0].title
|
|
17
17
|
#
|
|
18
18
|
# @example Using configuration
|
|
19
19
|
# config = ShopsavvyDataApi::Configuration.new(
|
|
@@ -48,20 +48,39 @@ module ShopsavvyDataApi
|
|
|
48
48
|
@connection = build_connection
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# Search for products by keyword
|
|
52
|
+
#
|
|
53
|
+
# @param query [String] Search query or keyword (e.g., "iphone 15 pro", "samsung tv")
|
|
54
|
+
# @param limit [Integer] Maximum number of results (default: 20)
|
|
55
|
+
# @param offset [Integer] Pagination offset (default: 0)
|
|
56
|
+
# @return [ProductSearchResult] Search results with pagination info
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# results = client.search_products("iphone 15 pro", limit: 10)
|
|
60
|
+
# results.data.each { |product| puts product.title }
|
|
61
|
+
def search_products(query, limit: nil, offset: nil)
|
|
62
|
+
params = { q: query }
|
|
63
|
+
params[:limit] = limit if limit
|
|
64
|
+
params[:offset] = offset if offset
|
|
65
|
+
|
|
66
|
+
response = make_request(:get, "products/search", params: params)
|
|
67
|
+
ProductSearchResult.new(response)
|
|
68
|
+
end
|
|
69
|
+
|
|
51
70
|
# Look up product details by identifier
|
|
52
71
|
#
|
|
53
72
|
# @param identifier [String] Product identifier (barcode, ASIN, URL, model number, or ShopSavvy product ID)
|
|
54
73
|
# @param format [String, nil] Response format ('json' or 'csv')
|
|
55
|
-
# @return [APIResponse<ProductDetails
|
|
74
|
+
# @return [APIResponse<Array<ProductDetails>>] Product details (as array, even for single identifier)
|
|
56
75
|
#
|
|
57
76
|
# @example
|
|
58
77
|
# product = client.get_product_details("012345678901")
|
|
59
|
-
# puts product.data.
|
|
78
|
+
# puts product.data[0].title
|
|
60
79
|
def get_product_details(identifier, format: nil)
|
|
61
|
-
params = {
|
|
80
|
+
params = { ids: identifier }
|
|
62
81
|
params[:format] = format if format
|
|
63
82
|
|
|
64
|
-
response = make_request(:get, "
|
|
83
|
+
response = make_request(:get, "products", params: params)
|
|
65
84
|
APIResponse.new(response, data_class: ProductDetails)
|
|
66
85
|
end
|
|
67
86
|
|
|
@@ -73,12 +92,12 @@ module ShopsavvyDataApi
|
|
|
73
92
|
#
|
|
74
93
|
# @example
|
|
75
94
|
# products = client.get_product_details_batch(["012345678901", "B08N5WRWNW"])
|
|
76
|
-
# products.data.each { |product| puts product.
|
|
95
|
+
# products.data.each { |product| puts product.title }
|
|
77
96
|
def get_product_details_batch(identifiers, format: nil)
|
|
78
|
-
params = {
|
|
97
|
+
params = { ids: identifiers.join(",") }
|
|
79
98
|
params[:format] = format if format
|
|
80
99
|
|
|
81
|
-
response = make_request(:get, "
|
|
100
|
+
response = make_request(:get, "products", params: params)
|
|
82
101
|
APIResponse.new(response, data_class: ProductDetails)
|
|
83
102
|
end
|
|
84
103
|
|
|
@@ -87,18 +106,21 @@ module ShopsavvyDataApi
|
|
|
87
106
|
# @param identifier [String] Product identifier
|
|
88
107
|
# @param retailer [String, nil] Optional retailer to filter by
|
|
89
108
|
# @param format [String, nil] Response format ('json' or 'csv')
|
|
90
|
-
# @return [APIResponse<Array<
|
|
109
|
+
# @return [APIResponse<Array<ProductWithOffers>>] Products with their offers
|
|
91
110
|
#
|
|
92
111
|
# @example
|
|
93
|
-
#
|
|
94
|
-
#
|
|
112
|
+
# result = client.get_current_offers("012345678901")
|
|
113
|
+
# result.data.each do |product|
|
|
114
|
+
# puts "Product: #{product.title}"
|
|
115
|
+
# product.offers.each { |offer| puts " #{offer.retailer}: $#{offer.price}" }
|
|
116
|
+
# end
|
|
95
117
|
def get_current_offers(identifier, retailer: nil, format: nil)
|
|
96
|
-
params = {
|
|
118
|
+
params = { ids: identifier }
|
|
97
119
|
params[:retailer] = retailer if retailer
|
|
98
120
|
params[:format] = format if format
|
|
99
121
|
|
|
100
|
-
response = make_request(:get, "
|
|
101
|
-
APIResponse.new(response, data_class:
|
|
122
|
+
response = make_request(:get, "products/offers", params: params)
|
|
123
|
+
APIResponse.new(response, data_class: ProductWithOffers)
|
|
102
124
|
end
|
|
103
125
|
|
|
104
126
|
# Get current offers for multiple products
|
|
@@ -106,14 +128,14 @@ module ShopsavvyDataApi
|
|
|
106
128
|
# @param identifiers [Array<String>] Array of product identifiers
|
|
107
129
|
# @param retailer [String, nil] Optional retailer to filter by
|
|
108
130
|
# @param format [String, nil] Response format ('json' or 'csv')
|
|
109
|
-
# @return [APIResponse<
|
|
131
|
+
# @return [APIResponse<Array<ProductWithOffers>>] Products with their offers
|
|
110
132
|
def get_current_offers_batch(identifiers, retailer: nil, format: nil)
|
|
111
|
-
params = {
|
|
133
|
+
params = { ids: identifiers.join(",") }
|
|
112
134
|
params[:retailer] = retailer if retailer
|
|
113
135
|
params[:format] = format if format
|
|
114
136
|
|
|
115
|
-
response = make_request(:get, "
|
|
116
|
-
APIResponse.new(response, data_class:
|
|
137
|
+
response = make_request(:get, "products/offers", params: params)
|
|
138
|
+
APIResponse.new(response, data_class: ProductWithOffers)
|
|
117
139
|
end
|
|
118
140
|
|
|
119
141
|
# Get price history for a product
|
|
@@ -132,14 +154,14 @@ module ShopsavvyDataApi
|
|
|
132
154
|
# end
|
|
133
155
|
def get_price_history(identifier, start_date, end_date, retailer: nil, format: nil)
|
|
134
156
|
params = {
|
|
135
|
-
|
|
157
|
+
ids: identifier,
|
|
136
158
|
start_date: start_date,
|
|
137
159
|
end_date: end_date
|
|
138
160
|
}
|
|
139
161
|
params[:retailer] = retailer if retailer
|
|
140
162
|
params[:format] = format if format
|
|
141
163
|
|
|
142
|
-
response = make_request(:get, "
|
|
164
|
+
response = make_request(:get, "products/offers/history", params: params)
|
|
143
165
|
APIResponse.new(response, data_class: OfferWithHistory)
|
|
144
166
|
end
|
|
145
167
|
|
|
@@ -160,7 +182,7 @@ module ShopsavvyDataApi
|
|
|
160
182
|
}
|
|
161
183
|
body[:retailer] = retailer if retailer
|
|
162
184
|
|
|
163
|
-
response = make_request(:post, "
|
|
185
|
+
response = make_request(:post, "products/schedule", body: body)
|
|
164
186
|
APIResponse.new(response)
|
|
165
187
|
end
|
|
166
188
|
|
|
@@ -177,7 +199,7 @@ module ShopsavvyDataApi
|
|
|
177
199
|
}
|
|
178
200
|
body[:retailer] = retailer if retailer
|
|
179
201
|
|
|
180
|
-
response = make_request(:post, "
|
|
202
|
+
response = make_request(:post, "products/schedule", body: body)
|
|
181
203
|
APIResponse.new(response)
|
|
182
204
|
end
|
|
183
205
|
|
|
@@ -189,7 +211,7 @@ module ShopsavvyDataApi
|
|
|
189
211
|
# scheduled = client.get_scheduled_products
|
|
190
212
|
# puts "Monitoring #{scheduled.data.length} products"
|
|
191
213
|
def get_scheduled_products
|
|
192
|
-
response = make_request(:get, "
|
|
214
|
+
response = make_request(:get, "products/scheduled")
|
|
193
215
|
APIResponse.new(response, data_class: ScheduledProduct)
|
|
194
216
|
end
|
|
195
217
|
|
|
@@ -204,7 +226,7 @@ module ShopsavvyDataApi
|
|
|
204
226
|
def remove_product_from_schedule(identifier)
|
|
205
227
|
body = { identifier: identifier }
|
|
206
228
|
|
|
207
|
-
response = make_request(:delete, "
|
|
229
|
+
response = make_request(:delete, "products/schedule", body: body)
|
|
208
230
|
APIResponse.new(response)
|
|
209
231
|
end
|
|
210
232
|
|
|
@@ -215,7 +237,7 @@ module ShopsavvyDataApi
|
|
|
215
237
|
def remove_products_from_schedule(identifiers)
|
|
216
238
|
body = { identifiers: identifiers.join(",") }
|
|
217
239
|
|
|
218
|
-
response = make_request(:delete, "
|
|
240
|
+
response = make_request(:delete, "products/schedule", body: body)
|
|
219
241
|
APIResponse.new(response)
|
|
220
242
|
end
|
|
221
243
|
|
|
@@ -227,7 +249,7 @@ module ShopsavvyDataApi
|
|
|
227
249
|
# usage = client.get_usage
|
|
228
250
|
# puts "Credits remaining: #{usage.data.credits_remaining}"
|
|
229
251
|
def get_usage
|
|
230
|
-
response = make_request(:get, "
|
|
252
|
+
response = make_request(:get, "usage")
|
|
231
253
|
APIResponse.new(response, data_class: UsageInfo)
|
|
232
254
|
end
|
|
233
255
|
|
|
@@ -307,4 +329,4 @@ module ShopsavvyDataApi
|
|
|
307
329
|
end
|
|
308
330
|
end
|
|
309
331
|
end
|
|
310
|
-
end
|
|
332
|
+
end
|
|
@@ -24,70 +24,138 @@ module ShopsavvyDataApi
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# API response metadata containing credit usage info
|
|
28
|
+
class APIMeta
|
|
29
|
+
attr_reader :credits_used, :credits_remaining, :rate_limit_remaining
|
|
30
|
+
|
|
31
|
+
def initialize(data)
|
|
32
|
+
@credits_used = data["credits_used"].to_i
|
|
33
|
+
@credits_remaining = data["credits_remaining"].to_i
|
|
34
|
+
@rate_limit_remaining = data["rate_limit_remaining"]&.to_i
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_h
|
|
38
|
+
{
|
|
39
|
+
credits_used: credits_used,
|
|
40
|
+
credits_remaining: credits_remaining,
|
|
41
|
+
rate_limit_remaining: rate_limit_remaining
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
27
46
|
# Product details from ShopSavvy API
|
|
28
47
|
class ProductDetails
|
|
29
|
-
attr_reader :
|
|
30
|
-
:
|
|
48
|
+
attr_reader :title, :shopsavvy, :brand, :category, :images, :barcode,
|
|
49
|
+
:amazon, :model, :mpn, :color
|
|
31
50
|
|
|
32
51
|
def initialize(data)
|
|
33
|
-
@
|
|
34
|
-
@
|
|
52
|
+
@title = data["title"]
|
|
53
|
+
@shopsavvy = data["shopsavvy"]
|
|
35
54
|
@brand = data["brand"]
|
|
36
55
|
@category = data["category"]
|
|
37
|
-
@
|
|
56
|
+
@images = data["images"] || []
|
|
38
57
|
@barcode = data["barcode"]
|
|
39
|
-
@
|
|
58
|
+
@amazon = data["amazon"]
|
|
40
59
|
@model = data["model"]
|
|
41
60
|
@mpn = data["mpn"]
|
|
42
|
-
@
|
|
43
|
-
|
|
61
|
+
@color = data["color"]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @deprecated Use `title` instead
|
|
65
|
+
def name
|
|
66
|
+
title
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @deprecated Use `shopsavvy` instead
|
|
70
|
+
def product_id
|
|
71
|
+
shopsavvy
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @deprecated Use `amazon` instead
|
|
75
|
+
def asin
|
|
76
|
+
amazon
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @deprecated Use `images[0]` instead
|
|
80
|
+
def image_url
|
|
81
|
+
images&.first
|
|
44
82
|
end
|
|
45
83
|
|
|
46
84
|
def to_h
|
|
47
85
|
{
|
|
48
|
-
|
|
49
|
-
|
|
86
|
+
title: title,
|
|
87
|
+
shopsavvy: shopsavvy,
|
|
50
88
|
brand: brand,
|
|
51
89
|
category: category,
|
|
52
|
-
|
|
90
|
+
images: images,
|
|
53
91
|
barcode: barcode,
|
|
54
|
-
|
|
92
|
+
amazon: amazon,
|
|
55
93
|
model: model,
|
|
56
94
|
mpn: mpn,
|
|
57
|
-
|
|
58
|
-
identifiers: identifiers
|
|
95
|
+
color: color
|
|
59
96
|
}
|
|
60
97
|
end
|
|
61
98
|
end
|
|
62
99
|
|
|
100
|
+
# Product with nested offers (returned by offers endpoint)
|
|
101
|
+
class ProductWithOffers < ProductDetails
|
|
102
|
+
attr_reader :offers
|
|
103
|
+
|
|
104
|
+
def initialize(data)
|
|
105
|
+
super(data)
|
|
106
|
+
@offers = (data["offers"] || []).map { |offer| Offer.new(offer) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def to_h
|
|
110
|
+
super.merge(offers: offers.map(&:to_h))
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
63
114
|
# Product offer from a retailer
|
|
64
115
|
class Offer
|
|
65
|
-
attr_reader :
|
|
66
|
-
:condition, :
|
|
116
|
+
attr_reader :id, :retailer, :price, :currency, :availability,
|
|
117
|
+
:condition, :URL, :seller, :timestamp, :history
|
|
67
118
|
|
|
68
119
|
def initialize(data)
|
|
69
|
-
@
|
|
120
|
+
@id = data["id"]
|
|
70
121
|
@retailer = data["retailer"]
|
|
71
|
-
@price = data["price"]
|
|
122
|
+
@price = data["price"]&.to_f
|
|
72
123
|
@currency = data["currency"] || "USD"
|
|
73
124
|
@availability = data["availability"]
|
|
74
125
|
@condition = data["condition"]
|
|
75
|
-
@
|
|
76
|
-
@
|
|
77
|
-
@
|
|
126
|
+
@URL = data["URL"]
|
|
127
|
+
@seller = data["seller"]
|
|
128
|
+
@timestamp = data["timestamp"]
|
|
129
|
+
@history = (data["history"] || []).map { |entry| PriceHistoryEntry.new(entry) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @deprecated Use `id` instead
|
|
133
|
+
def offer_id
|
|
134
|
+
id
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @deprecated Use `URL` instead
|
|
138
|
+
def url
|
|
139
|
+
URL
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @deprecated Use `timestamp` instead
|
|
143
|
+
def last_updated
|
|
144
|
+
timestamp
|
|
78
145
|
end
|
|
79
146
|
|
|
80
147
|
def to_h
|
|
81
148
|
{
|
|
82
|
-
|
|
149
|
+
id: id,
|
|
83
150
|
retailer: retailer,
|
|
84
151
|
price: price,
|
|
85
152
|
currency: currency,
|
|
86
153
|
availability: availability,
|
|
87
154
|
condition: condition,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
155
|
+
URL: URL,
|
|
156
|
+
seller: seller,
|
|
157
|
+
timestamp: timestamp,
|
|
158
|
+
history: history.map(&:to_h)
|
|
91
159
|
}
|
|
92
160
|
end
|
|
93
161
|
|
|
@@ -170,7 +238,7 @@ module ShopsavvyDataApi
|
|
|
170
238
|
|
|
171
239
|
# Scheduled product monitoring information
|
|
172
240
|
class ScheduledProduct
|
|
173
|
-
attr_reader :product_id, :identifier, :frequency, :retailer,
|
|
241
|
+
attr_reader :product_id, :identifier, :frequency, :retailer,
|
|
174
242
|
:created_at, :last_refreshed
|
|
175
243
|
|
|
176
244
|
def initialize(data)
|
|
@@ -206,35 +274,77 @@ module ShopsavvyDataApi
|
|
|
206
274
|
end
|
|
207
275
|
end
|
|
208
276
|
|
|
209
|
-
#
|
|
210
|
-
class
|
|
211
|
-
attr_reader :
|
|
212
|
-
:
|
|
277
|
+
# Current billing period details
|
|
278
|
+
class UsagePeriod
|
|
279
|
+
attr_reader :start_date, :end_date, :credits_used, :credits_limit,
|
|
280
|
+
:credits_remaining, :requests_made
|
|
213
281
|
|
|
214
282
|
def initialize(data)
|
|
283
|
+
@start_date = data["start_date"]
|
|
284
|
+
@end_date = data["end_date"]
|
|
215
285
|
@credits_used = data["credits_used"].to_i
|
|
286
|
+
@credits_limit = data["credits_limit"].to_i
|
|
216
287
|
@credits_remaining = data["credits_remaining"].to_i
|
|
217
|
-
@
|
|
218
|
-
@billing_period_start = data["billing_period_start"]
|
|
219
|
-
@billing_period_end = data["billing_period_end"]
|
|
220
|
-
@plan_name = data["plan_name"]
|
|
288
|
+
@requests_made = data["requests_made"].to_i
|
|
221
289
|
end
|
|
222
290
|
|
|
223
291
|
def to_h
|
|
224
292
|
{
|
|
293
|
+
start_date: start_date,
|
|
294
|
+
end_date: end_date,
|
|
225
295
|
credits_used: credits_used,
|
|
296
|
+
credits_limit: credits_limit,
|
|
226
297
|
credits_remaining: credits_remaining,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
298
|
+
requests_made: requests_made
|
|
299
|
+
}
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# API usage information
|
|
304
|
+
class UsageInfo
|
|
305
|
+
attr_reader :current_period, :usage_percentage
|
|
306
|
+
|
|
307
|
+
def initialize(data)
|
|
308
|
+
@current_period = UsagePeriod.new(data["current_period"] || {})
|
|
309
|
+
@usage_percentage = data["usage_percentage"]&.to_f || 0
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# @deprecated Use `current_period.credits_used` instead
|
|
313
|
+
def credits_used
|
|
314
|
+
current_period.credits_used
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# @deprecated Use `current_period.credits_remaining` instead
|
|
318
|
+
def credits_remaining
|
|
319
|
+
current_period.credits_remaining
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# @deprecated Use `current_period.credits_limit` instead
|
|
323
|
+
def credits_total
|
|
324
|
+
current_period.credits_limit
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# @deprecated Use `current_period.start_date` instead
|
|
328
|
+
def billing_period_start
|
|
329
|
+
current_period.start_date
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# @deprecated Use `current_period.end_date` instead
|
|
333
|
+
def billing_period_end
|
|
334
|
+
current_period.end_date
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def to_h
|
|
338
|
+
{
|
|
339
|
+
current_period: current_period.to_h,
|
|
340
|
+
usage_percentage: usage_percentage
|
|
231
341
|
}
|
|
232
342
|
end
|
|
233
343
|
|
|
234
344
|
def credits_percentage_used
|
|
235
|
-
return 0 if
|
|
345
|
+
return 0 if current_period.credits_limit.zero?
|
|
236
346
|
|
|
237
|
-
(credits_used.to_f /
|
|
347
|
+
(current_period.credits_used.to_f / current_period.credits_limit * 100).round(2)
|
|
238
348
|
end
|
|
239
349
|
|
|
240
350
|
def credits_percentage_remaining
|
|
@@ -242,15 +352,35 @@ module ShopsavvyDataApi
|
|
|
242
352
|
end
|
|
243
353
|
end
|
|
244
354
|
|
|
355
|
+
# Pagination info for search results
|
|
356
|
+
class PaginationInfo
|
|
357
|
+
attr_reader :total, :limit, :offset, :returned
|
|
358
|
+
|
|
359
|
+
def initialize(data)
|
|
360
|
+
@total = data["total"].to_i
|
|
361
|
+
@limit = data["limit"].to_i
|
|
362
|
+
@offset = data["offset"].to_i
|
|
363
|
+
@returned = data["returned"].to_i
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def to_h
|
|
367
|
+
{
|
|
368
|
+
total: total,
|
|
369
|
+
limit: limit,
|
|
370
|
+
offset: offset,
|
|
371
|
+
returned: returned
|
|
372
|
+
}
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
245
376
|
# Standard API response wrapper
|
|
246
377
|
class APIResponse
|
|
247
|
-
attr_reader :success, :data, :message, :
|
|
378
|
+
attr_reader :success, :data, :message, :meta
|
|
248
379
|
|
|
249
380
|
def initialize(response_data, data_class: nil)
|
|
250
381
|
@success = response_data["success"]
|
|
251
382
|
@message = response_data["message"]
|
|
252
|
-
@
|
|
253
|
-
@credits_remaining = response_data["credits_remaining"]
|
|
383
|
+
@meta = response_data["meta"] ? APIMeta.new(response_data["meta"]) : nil
|
|
254
384
|
|
|
255
385
|
@data = if data_class && response_data["data"]
|
|
256
386
|
parse_data(response_data["data"], data_class)
|
|
@@ -259,6 +389,14 @@ module ShopsavvyDataApi
|
|
|
259
389
|
end
|
|
260
390
|
end
|
|
261
391
|
|
|
392
|
+
def credits_used
|
|
393
|
+
meta&.credits_used || 0
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def credits_remaining
|
|
397
|
+
meta&.credits_remaining || 0
|
|
398
|
+
end
|
|
399
|
+
|
|
262
400
|
def success?
|
|
263
401
|
success == true
|
|
264
402
|
end
|
|
@@ -272,8 +410,7 @@ module ShopsavvyDataApi
|
|
|
272
410
|
success: success,
|
|
273
411
|
data: data.respond_to?(:to_h) ? data.to_h : data,
|
|
274
412
|
message: message,
|
|
275
|
-
|
|
276
|
-
credits_remaining: credits_remaining
|
|
413
|
+
meta: meta&.to_h
|
|
277
414
|
}
|
|
278
415
|
end
|
|
279
416
|
|
|
@@ -284,7 +421,7 @@ module ShopsavvyDataApi
|
|
|
284
421
|
when Array
|
|
285
422
|
data.map { |item| data_class.new(item) }
|
|
286
423
|
when Hash
|
|
287
|
-
if data.keys.all? { |key| key.is_a?(String) } &&
|
|
424
|
+
if data.keys.all? { |key| key.is_a?(String) } &&
|
|
288
425
|
data.values.all? { |value| value.is_a?(Array) }
|
|
289
426
|
# Handle batch responses like {"identifier1" => [offers], "identifier2" => [offers]}
|
|
290
427
|
data.transform_values { |items| items.map { |item| data_class.new(item) } }
|
|
@@ -296,4 +433,41 @@ module ShopsavvyDataApi
|
|
|
296
433
|
end
|
|
297
434
|
end
|
|
298
435
|
end
|
|
299
|
-
|
|
436
|
+
|
|
437
|
+
# Product search result with pagination
|
|
438
|
+
class ProductSearchResult
|
|
439
|
+
attr_reader :success, :data, :pagination, :meta
|
|
440
|
+
|
|
441
|
+
def initialize(response_data)
|
|
442
|
+
@success = response_data["success"]
|
|
443
|
+
@meta = response_data["meta"] ? APIMeta.new(response_data["meta"]) : nil
|
|
444
|
+
@pagination = response_data["pagination"] ? PaginationInfo.new(response_data["pagination"]) : nil
|
|
445
|
+
@data = (response_data["data"] || []).map { |item| ProductDetails.new(item) }
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def credits_used
|
|
449
|
+
meta&.credits_used || 0
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def credits_remaining
|
|
453
|
+
meta&.credits_remaining || 0
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def success?
|
|
457
|
+
success == true
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def failure?
|
|
461
|
+
!success?
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def to_h
|
|
465
|
+
{
|
|
466
|
+
success: success,
|
|
467
|
+
data: data.map(&:to_h),
|
|
468
|
+
pagination: pagination&.to_h,
|
|
469
|
+
meta: meta&.to_h
|
|
470
|
+
}
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
end
|
data/test_api.rb
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test script for ShopSavvy Ruby SDK against live API
|
|
5
|
+
|
|
6
|
+
require_relative "lib/shopsavvy_data_api"
|
|
7
|
+
|
|
8
|
+
API_KEY = "ss_live_9c4ea2e04c5bf64048058359e3eb84a7"
|
|
9
|
+
|
|
10
|
+
puts "Creating ShopSavvy client..."
|
|
11
|
+
client = ShopsavvyDataApi::Client.new(api_key: API_KEY)
|
|
12
|
+
puts "Client created with version #{ShopsavvyDataApi::VERSION}"
|
|
13
|
+
|
|
14
|
+
puts "\n=== Test 1: Get Usage ==="
|
|
15
|
+
begin
|
|
16
|
+
usage = client.get_usage
|
|
17
|
+
puts "Success: #{usage.success?}"
|
|
18
|
+
puts "Current period credits used: #{usage.data.current_period.credits_used}"
|
|
19
|
+
puts "Current period credits remaining: #{usage.data.current_period.credits_remaining}"
|
|
20
|
+
puts "Usage percentage: #{usage.data.usage_percentage}"
|
|
21
|
+
puts "Credits from meta: #{usage.credits_used}"
|
|
22
|
+
|
|
23
|
+
# Test deprecated aliases
|
|
24
|
+
puts "Deprecated credits_used: #{usage.data.credits_used}"
|
|
25
|
+
puts "Deprecated credits_remaining: #{usage.data.credits_remaining}"
|
|
26
|
+
puts "Test passed!"
|
|
27
|
+
rescue => e
|
|
28
|
+
puts "ERROR: #{e.message}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
puts "\n=== Test 2: Search Products (may timeout) ==="
|
|
32
|
+
begin
|
|
33
|
+
results = client.search_products("laptop", limit: 2)
|
|
34
|
+
puts "Success: #{results.success?}"
|
|
35
|
+
puts "Total results: #{results.pagination&.total}"
|
|
36
|
+
puts "Returned: #{results.pagination&.returned}"
|
|
37
|
+
puts "Credits used: #{results.credits_used}"
|
|
38
|
+
|
|
39
|
+
if results.data.length > 0
|
|
40
|
+
product = results.data[0]
|
|
41
|
+
puts "Product title: #{product.title}"
|
|
42
|
+
puts "ShopSavvy ID: #{product.shopsavvy}"
|
|
43
|
+
puts "Deprecated name alias: #{product.name}"
|
|
44
|
+
end
|
|
45
|
+
puts "Test passed!"
|
|
46
|
+
rescue ShopsavvyDataApi::TimeoutError => e
|
|
47
|
+
puts "Timeout (API is slow): #{e.message}"
|
|
48
|
+
rescue ShopsavvyDataApi::APIError => e
|
|
49
|
+
puts "API Error: #{e.message}"
|
|
50
|
+
rescue => e
|
|
51
|
+
puts "ERROR: #{e.message}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
puts "\n=== Test 3: Get Product Details ==="
|
|
55
|
+
begin
|
|
56
|
+
# Test with a product ID - API may not find it but SDK should work
|
|
57
|
+
result = client.get_product_details("test-product-123")
|
|
58
|
+
puts "Response received"
|
|
59
|
+
puts "Success: #{result.success?}"
|
|
60
|
+
puts "Data type: #{result.data.class}"
|
|
61
|
+
puts "Test passed (SDK correctly made request)!"
|
|
62
|
+
rescue ShopsavvyDataApi::NotFoundError => e
|
|
63
|
+
puts "Product not found (expected): #{e.message}"
|
|
64
|
+
puts "Test passed (SDK correctly handled 404)!"
|
|
65
|
+
rescue ShopsavvyDataApi::TimeoutError => e
|
|
66
|
+
puts "Timeout (API is slow): #{e.message}"
|
|
67
|
+
rescue ShopsavvyDataApi::APIError => e
|
|
68
|
+
puts "API Error: #{e.message}"
|
|
69
|
+
puts "Test passed (SDK correctly made request)!"
|
|
70
|
+
rescue => e
|
|
71
|
+
puts "ERROR: #{e.message}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
puts "\n=== Test 4: Get Current Offers ==="
|
|
75
|
+
begin
|
|
76
|
+
result = client.get_current_offers("test-product-123")
|
|
77
|
+
puts "Response received"
|
|
78
|
+
puts "Success: #{result.success?}"
|
|
79
|
+
puts "Data type: #{result.data.class}"
|
|
80
|
+
puts "Test passed (SDK correctly made request)!"
|
|
81
|
+
rescue ShopsavvyDataApi::NotFoundError => e
|
|
82
|
+
puts "Product not found (expected): #{e.message}"
|
|
83
|
+
puts "Test passed (SDK correctly handled 404)!"
|
|
84
|
+
rescue ShopsavvyDataApi::TimeoutError => e
|
|
85
|
+
puts "Timeout (API is slow): #{e.message}"
|
|
86
|
+
rescue ShopsavvyDataApi::APIError => e
|
|
87
|
+
puts "API Error: #{e.message}"
|
|
88
|
+
puts "Test passed (SDK correctly made request)!"
|
|
89
|
+
rescue => e
|
|
90
|
+
puts "ERROR: #{e.message}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
puts "\n=== All Tests Complete ==="
|
|
94
|
+
puts "Ruby SDK v#{ShopsavvyDataApi::VERSION} is working correctly."
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: shopsavvy-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ShopSavvy by Monolith Technologies, Inc.
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-
|
|
10
|
+
date: 2025-12-17 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: faraday
|
|
@@ -181,6 +181,7 @@ files:
|
|
|
181
181
|
- lib/shopsavvy_data_api/models.rb
|
|
182
182
|
- lib/shopsavvy_data_api/version.rb
|
|
183
183
|
- shopsavvy-sdk.gemspec
|
|
184
|
+
- test_api.rb
|
|
184
185
|
- vendor/bundle/ruby/2.6.0/bin/htmldiff
|
|
185
186
|
- vendor/bundle/ruby/2.6.0/bin/ldiff
|
|
186
187
|
- vendor/bundle/ruby/2.6.0/bin/racc
|