carddb 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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rspec_status +96 -0
- data/.rubocop.yml +72 -0
- data/AGENTS.md +27 -0
- data/CHANGELOG.md +127 -0
- data/LICENSE.txt +21 -0
- data/README.md +732 -0
- data/Rakefile +10 -0
- data/examples/basic_usage.rb +60 -0
- data/examples/filtering.rb +93 -0
- data/examples/pagination.rb +58 -0
- data/lib/carddb/batch.rb +287 -0
- data/lib/carddb/cache.rb +120 -0
- data/lib/carddb/client.rb +139 -0
- data/lib/carddb/collection.rb +919 -0
- data/lib/carddb/configuration.rb +185 -0
- data/lib/carddb/connection.rb +224 -0
- data/lib/carddb/errors.rb +85 -0
- data/lib/carddb/filter_builder.rb +214 -0
- data/lib/carddb/query_builder.rb +658 -0
- data/lib/carddb/resources/base.rb +86 -0
- data/lib/carddb/resources/datasets.rb +132 -0
- data/lib/carddb/resources/decks.rb +125 -0
- data/lib/carddb/resources/games.rb +111 -0
- data/lib/carddb/resources/publishers.rb +86 -0
- data/lib/carddb/resources/records.rb +239 -0
- data/lib/carddb/resources/rules.rb +49 -0
- data/lib/carddb/version.rb +5 -0
- data/lib/carddb.rb +160 -0
- metadata +102 -0
data/README.md
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
# CardDB Ruby Client
|
|
2
|
+
|
|
3
|
+
A Ruby client library for the [CardDB](https://carddb.xtda.org) GraphQL API. Search and fetch card game data with an expressive filter DSL.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'carddb'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install from source:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem 'carddb', git: 'https://github.com/xtda/carddb-ruby'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
require 'carddb'
|
|
23
|
+
|
|
24
|
+
# Configure globally (optional - increases rate limits)
|
|
25
|
+
CardDB.configure do |config|
|
|
26
|
+
config.api_key = "carddb_your_api_key"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Search for games
|
|
30
|
+
games = CardDB.games.search(publisher_slug: "pokemon-company")
|
|
31
|
+
games.each { |game| puts game.name }
|
|
32
|
+
|
|
33
|
+
# Search for records with filter DSL
|
|
34
|
+
records = CardDB.records.search(
|
|
35
|
+
publisher_slug: "pokemon-company",
|
|
36
|
+
game_key: "pokemon-tcg",
|
|
37
|
+
dataset_key: "cards"
|
|
38
|
+
) do
|
|
39
|
+
where(types: contains("Pokemon"))
|
|
40
|
+
where(hp: gte(100))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
records.each { |record| puts record.name }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
### Global Configuration
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
CardDB.configure do |config|
|
|
52
|
+
# Authentication (optional but recommended)
|
|
53
|
+
config.api_key = "carddb_your_api_key"
|
|
54
|
+
|
|
55
|
+
# Endpoint (defaults to production)
|
|
56
|
+
config.endpoint = "https://carddb.xtda.org/query"
|
|
57
|
+
|
|
58
|
+
# Timeouts
|
|
59
|
+
config.timeout = 30 # Request timeout in seconds
|
|
60
|
+
config.open_timeout = 10 # Connection timeout
|
|
61
|
+
|
|
62
|
+
# Defaults (reduce repetition in queries)
|
|
63
|
+
config.default_publisher = "pokemon-company"
|
|
64
|
+
config.default_game = "pokemon-tcg"
|
|
65
|
+
|
|
66
|
+
# Restrictions (optional - raises error if outside scope)
|
|
67
|
+
config.allowed_publishers = ["pokemon-company"]
|
|
68
|
+
config.allowed_games = {
|
|
69
|
+
"pokemon-company" => ["pokemon-tcg"]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Logging (optional)
|
|
73
|
+
config.logger = Logger.new(STDOUT)
|
|
74
|
+
config.log_level = :info # :debug, :info, :warn, :error
|
|
75
|
+
|
|
76
|
+
# Auto-retry on rate limit (optional, disabled by default)
|
|
77
|
+
config.retry_on_rate_limit = true
|
|
78
|
+
config.max_retries = 3
|
|
79
|
+
|
|
80
|
+
# Caching (optional, disabled by default)
|
|
81
|
+
config.cache = CardDB::MemoryCache.new
|
|
82
|
+
config.cache_ttl = 300 # 5 minutes
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Per-Client Configuration
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
client = CardDB::Client.new(
|
|
90
|
+
api_key: "carddb_different_key",
|
|
91
|
+
default_publisher: "wizards",
|
|
92
|
+
default_game: "magic-the-gathering"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
records = client.records.search(dataset_key: "cards")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Usage
|
|
99
|
+
|
|
100
|
+
### Publishers
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# Search publishers
|
|
104
|
+
publishers = CardDB.publishers.search
|
|
105
|
+
publishers = CardDB.publishers.search(search: "pokemon")
|
|
106
|
+
|
|
107
|
+
# Fetch by ID or slug
|
|
108
|
+
publisher = CardDB.publishers.fetch(id: "uuid-here")
|
|
109
|
+
publisher = CardDB.publishers.fetch(slug: "pokemon-company")
|
|
110
|
+
|
|
111
|
+
# Fetch multiple by slugs
|
|
112
|
+
publishers = CardDB.publishers.fetch_many(["pokemon-company", "wizards"])
|
|
113
|
+
|
|
114
|
+
# Publisher status is exposed as `ACTIVE` or `DEACTIVATED`
|
|
115
|
+
puts publisher.status
|
|
116
|
+
|
|
117
|
+
# Navigate to games
|
|
118
|
+
publisher.games.each { |game| puts game.name }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Games
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
# Search games
|
|
125
|
+
games = CardDB.games.search
|
|
126
|
+
games = CardDB.games.search(publisher_slug: "pokemon-company")
|
|
127
|
+
games = CardDB.games.search(search: "pokemon")
|
|
128
|
+
|
|
129
|
+
# Fetch by ID
|
|
130
|
+
game = CardDB.games.fetch("uuid-here")
|
|
131
|
+
|
|
132
|
+
# Get by keys
|
|
133
|
+
game = CardDB.games.get(
|
|
134
|
+
publisher_slug: "pokemon-company",
|
|
135
|
+
game_key: "pokemon-tcg"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Navigate to datasets
|
|
139
|
+
game.datasets.each { |dataset| puts dataset.name }
|
|
140
|
+
|
|
141
|
+
# Navigate to rule-related datasets for this game
|
|
142
|
+
game.datasets(purpose: "RULES").each { |dataset| puts dataset.name }
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Datasets
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
# Search datasets
|
|
149
|
+
datasets = CardDB.datasets.search
|
|
150
|
+
datasets = CardDB.datasets.search(game_key: "pokemon-tcg")
|
|
151
|
+
datasets = CardDB.datasets.search(purpose: "RULES")
|
|
152
|
+
|
|
153
|
+
# Fetch by ID (includes schema)
|
|
154
|
+
dataset = CardDB.datasets.fetch("uuid-here")
|
|
155
|
+
|
|
156
|
+
# Get by keys (includes schema)
|
|
157
|
+
dataset = CardDB.datasets.get(
|
|
158
|
+
publisher_slug: "pokemon-company",
|
|
159
|
+
game_key: "pokemon-tcg",
|
|
160
|
+
dataset_key: "cards"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Get schema only
|
|
164
|
+
schema = CardDB.datasets.schema(
|
|
165
|
+
dataset_key: "cards",
|
|
166
|
+
publisher_slug: "pokemon-company",
|
|
167
|
+
game_key: "pokemon-tcg"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
schema.fields.each do |field|
|
|
171
|
+
puts "#{field.key}: #{field.type} (filterable: #{field.filterable?})"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Schema-aware helpers
|
|
175
|
+
dataset.field_keys # All field keys
|
|
176
|
+
dataset.filterable_fields # Filterable field keys
|
|
177
|
+
dataset.searchable_fields # Searchable field keys
|
|
178
|
+
dataset.identifier_field # The identifier field key (e.g., "card_id")
|
|
179
|
+
|
|
180
|
+
dataset.filterable?(:hp) # Check if field is filterable
|
|
181
|
+
dataset.searchable?(:name) # Check if field is searchable
|
|
182
|
+
dataset.field(:hp) # Get field info by key
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Rules and Formats
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
# List rule-related datasets for a game
|
|
189
|
+
rule_datasets = CardDB.rules.datasets(
|
|
190
|
+
publisher_slug: "pokemon-company",
|
|
191
|
+
game_key: "pokemon-tcg"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# List deck/game formats from the standard formats dataset
|
|
195
|
+
formats = CardDB.rules.formats(
|
|
196
|
+
publisher_slug: "pokemon-company",
|
|
197
|
+
game_key: "pokemon-tcg",
|
|
198
|
+
first: 100
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
formats.each { |format| puts format.name }
|
|
202
|
+
|
|
203
|
+
# List records from the standard rules dataset
|
|
204
|
+
rules = CardDB.rules.list(
|
|
205
|
+
publisher_slug: "pokemon-company",
|
|
206
|
+
game_key: "pokemon-tcg"
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Records
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
# Search with filter DSL
|
|
214
|
+
records = CardDB.records.search(
|
|
215
|
+
publisher_slug: "pokemon-company",
|
|
216
|
+
game_key: "pokemon-tcg",
|
|
217
|
+
dataset_key: "cards"
|
|
218
|
+
) do
|
|
219
|
+
where(name: ilike("%pikachu%"))
|
|
220
|
+
where(hp: gte(60))
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Search with hash filter
|
|
224
|
+
records = CardDB.records.search(
|
|
225
|
+
publisher_slug: "pokemon-company",
|
|
226
|
+
game_key: "pokemon-tcg",
|
|
227
|
+
dataset_key: "cards",
|
|
228
|
+
filter: { "name" => { "ilike" => "%pikachu%" } }
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Fetch by ID
|
|
232
|
+
record = CardDB.records.fetch("uuid-here")
|
|
233
|
+
|
|
234
|
+
# Fetch multiple
|
|
235
|
+
records = CardDB.records.fetch_many(["uuid-1", "uuid-2"])
|
|
236
|
+
|
|
237
|
+
# Get by identifier value (uses dataset's identifier field)
|
|
238
|
+
record = CardDB.records.get(
|
|
239
|
+
identifier: "xy1-1",
|
|
240
|
+
dataset_key: "cards",
|
|
241
|
+
publisher_slug: "pokemon-company",
|
|
242
|
+
game_key: "pokemon-tcg"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Get multiple by identifier value
|
|
246
|
+
records = CardDB.records.get_many(
|
|
247
|
+
identifiers: ["xy1-1", "xy1-2"],
|
|
248
|
+
dataset_key: "cards",
|
|
249
|
+
publisher_slug: "pokemon-company",
|
|
250
|
+
game_key: "pokemon-tcg"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Access record data
|
|
254
|
+
record["name"] # Bracket notation
|
|
255
|
+
record.name # Method notation (shorthand)
|
|
256
|
+
record.hp
|
|
257
|
+
record.record_data # Full data hash
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Filter DSL
|
|
261
|
+
|
|
262
|
+
The filter DSL provides an expressive way to build queries:
|
|
263
|
+
|
|
264
|
+
### Basic Filters
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
records = CardDB.records.search(dataset_key: "cards") do
|
|
268
|
+
# Simple equality
|
|
269
|
+
where(name: "Pikachu")
|
|
270
|
+
|
|
271
|
+
# With operators
|
|
272
|
+
where(hp: gte(100))
|
|
273
|
+
where(cmc: lte(3))
|
|
274
|
+
|
|
275
|
+
# Pattern matching (case-insensitive)
|
|
276
|
+
where(name: ilike("%bolt%"))
|
|
277
|
+
|
|
278
|
+
# Array contains
|
|
279
|
+
where(types: contains("Pokemon"))
|
|
280
|
+
|
|
281
|
+
# In list
|
|
282
|
+
where(rarity: within(["rare", "ultra-rare"]))
|
|
283
|
+
|
|
284
|
+
# Null checks
|
|
285
|
+
where(flavor_text: is_not_null)
|
|
286
|
+
end
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Available Operators
|
|
290
|
+
|
|
291
|
+
| Operator | Description | Example |
|
|
292
|
+
|----------|-------------|---------|
|
|
293
|
+
| `eq(value)` | Equals | `where(name: eq("Pikachu"))` |
|
|
294
|
+
| `neq(value)` | Not equals | `where(type: neq("trainer"))` |
|
|
295
|
+
| `gt(value)` | Greater than | `where(hp: gt(100))` |
|
|
296
|
+
| `gte(value)` | Greater than or equal | `where(hp: gte(100))` |
|
|
297
|
+
| `lt(value)` | Less than | `where(cmc: lt(5))` |
|
|
298
|
+
| `lte(value)` | Less than or equal | `where(cmc: lte(3))` |
|
|
299
|
+
| `within(array)` | Value in array | `where(rarity: within(["rare", "mythic"]))` |
|
|
300
|
+
| `not_within(array)` | Value not in array | `where(color: not_within(["black"]))` |
|
|
301
|
+
| `contains(value)` | Array contains | `where(types: contains("Creature"))` |
|
|
302
|
+
| `like(pattern)` | Case-sensitive pattern | `where(name: like("Lightning%"))` |
|
|
303
|
+
| `ilike(pattern)` | Case-insensitive pattern | `where(name: ilike("%bolt%"))` |
|
|
304
|
+
| `is_null` | Is null | `where(deleted_at: is_null)` |
|
|
305
|
+
| `is_not_null` | Is not null | `where(flavor_text: is_not_null)` |
|
|
306
|
+
|
|
307
|
+
### Boolean Logic
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
# OR conditions
|
|
311
|
+
records = CardDB.records.search(dataset_key: "cards") do
|
|
312
|
+
where(type: "creature")
|
|
313
|
+
any do
|
|
314
|
+
where(color: "red")
|
|
315
|
+
where(color: "blue")
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Multiple conditions (implicit AND)
|
|
320
|
+
records = CardDB.records.search(dataset_key: "cards") do
|
|
321
|
+
where(type: "creature")
|
|
322
|
+
where(hp: gte(100))
|
|
323
|
+
where(rarity: "rare")
|
|
324
|
+
end
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Nested Fields
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
# HASH fields (dot notation)
|
|
331
|
+
records = CardDB.records.search(dataset_key: "cards") do
|
|
332
|
+
where(stats: { power: gte(3) })
|
|
333
|
+
# or
|
|
334
|
+
where("stats.power" => gte(3))
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Array of objects (any element matches)
|
|
338
|
+
records = CardDB.records.search(dataset_key: "cards") do
|
|
339
|
+
where_any(:attacks, damage: gte(50))
|
|
340
|
+
where_any(:attacks, name: ilike("%thunder%"))
|
|
341
|
+
end
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Link Filtering
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
# Filter on linked records
|
|
348
|
+
records = CardDB.records.search(dataset_key: "cards") do
|
|
349
|
+
where_link(:set_id, code: "DMU")
|
|
350
|
+
where_link(:set_id, name: ilike("%dominaria%"))
|
|
351
|
+
end
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Link Resolution
|
|
355
|
+
|
|
356
|
+
Include data from linked records in your search results using `resolve_links`.
|
|
357
|
+
|
|
358
|
+
### Path Syntax
|
|
359
|
+
|
|
360
|
+
| Path | Description | Example |
|
|
361
|
+
|------|-------------|---------|
|
|
362
|
+
| `field` | Single link field | `set_id` |
|
|
363
|
+
| `array_field` | Array of links | `reprints` |
|
|
364
|
+
| `hash.field` | Link inside a hash | `metadata.artist_id` |
|
|
365
|
+
| `array.field` | Link inside each hash in an array | `abilities.type_id` |
|
|
366
|
+
| `link.nested` | Nested resolution | `set_id.publisher_id` |
|
|
367
|
+
|
|
368
|
+
### Response Structure
|
|
369
|
+
|
|
370
|
+
Each resolved link (`ResolvedLink`) contains:
|
|
371
|
+
- `field` - The path that was resolved
|
|
372
|
+
- `link_field_key` - The field in target dataset matched against
|
|
373
|
+
- `values` - Array of all link values found
|
|
374
|
+
- `records` - Parallel array of resolved records (nil for unresolved)
|
|
375
|
+
|
|
376
|
+
For convenience, single-value links also support:
|
|
377
|
+
- `value` - First value (shorthand for `values.first`)
|
|
378
|
+
- `record` - First record (shorthand for `records.first`)
|
|
379
|
+
|
|
380
|
+
### Single Link Field
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
records = CardDB.records.search(
|
|
384
|
+
dataset_key: "cards",
|
|
385
|
+
resolve_links: ["set_id"]
|
|
386
|
+
) do
|
|
387
|
+
where_link(:set_id, code: "DMU")
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
records.each do |record|
|
|
391
|
+
puts record.name
|
|
392
|
+
if (set = record.resolved_links["set_id"])
|
|
393
|
+
# Use .record shorthand for single links
|
|
394
|
+
puts "From set: #{set.record.name}"
|
|
395
|
+
# Or use .records array
|
|
396
|
+
puts "From set: #{set.records.first.name}"
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Array of Links
|
|
402
|
+
|
|
403
|
+
Resolve multiple linked records from an array field:
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
records = CardDB.records.search(
|
|
407
|
+
dataset_key: "cards",
|
|
408
|
+
resolve_links: ["reprints"]
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
records.each do |record|
|
|
412
|
+
if (reprints_link = record.resolved_links["reprints"])
|
|
413
|
+
puts "#{record.name} has #{reprints_link.values.length} reprints:"
|
|
414
|
+
|
|
415
|
+
reprints_link.values.each_with_index do |value, i|
|
|
416
|
+
reprint = reprints_link.records[i]
|
|
417
|
+
if reprint
|
|
418
|
+
puts " - #{reprint.name}"
|
|
419
|
+
else
|
|
420
|
+
puts " - #{value} (not found)"
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Links Inside Hash Fields
|
|
428
|
+
|
|
429
|
+
Resolve a link nested inside a hash/object field:
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
records = CardDB.records.search(
|
|
433
|
+
dataset_key: "cards",
|
|
434
|
+
resolve_links: ["metadata.artist_id"]
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
records.each do |record|
|
|
438
|
+
if (artist_link = record.resolved_links["metadata.artist_id"])
|
|
439
|
+
puts "Art by: #{artist_link.record&.name || 'Unknown'}"
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Links Inside Arrays of Hashes
|
|
445
|
+
|
|
446
|
+
Resolve links from each object in an array field:
|
|
447
|
+
|
|
448
|
+
```ruby
|
|
449
|
+
records = CardDB.records.search(
|
|
450
|
+
dataset_key: "cards",
|
|
451
|
+
resolve_links: ["abilities.type_id"]
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
records.each do |record|
|
|
455
|
+
if (types_link = record.resolved_links["abilities.type_id"])
|
|
456
|
+
type_names = types_link.records.compact.map(&:name)
|
|
457
|
+
puts "Ability types: #{type_names.join(', ')}"
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Nested Resolution
|
|
463
|
+
|
|
464
|
+
Chain through multiple links:
|
|
465
|
+
|
|
466
|
+
```ruby
|
|
467
|
+
records = CardDB.records.search(
|
|
468
|
+
dataset_key: "cards",
|
|
469
|
+
resolve_links: ["set_id", "set_id.publisher_id"]
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
records.each do |record|
|
|
473
|
+
set_link = record.resolved_links["set_id"]
|
|
474
|
+
publisher_link = record.resolved_links["set_id.publisher_id"]
|
|
475
|
+
|
|
476
|
+
set_name = set_link&.record&.name || "Unknown"
|
|
477
|
+
publisher_name = publisher_link&.record&.name || "Unknown"
|
|
478
|
+
|
|
479
|
+
puts "#{record.name} from #{set_name} (#{publisher_name})"
|
|
480
|
+
end
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Combining with Filters
|
|
484
|
+
|
|
485
|
+
Link resolution works alongside link filtering:
|
|
486
|
+
|
|
487
|
+
```ruby
|
|
488
|
+
records = CardDB.records.search(
|
|
489
|
+
dataset_key: "cards",
|
|
490
|
+
resolve_links: ["set_id", "set_id.publisher_id", "reprints"]
|
|
491
|
+
) do
|
|
492
|
+
where_link(:set_id, code: "DMU")
|
|
493
|
+
where(rarity: "mythic")
|
|
494
|
+
end
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
## Pagination
|
|
498
|
+
|
|
499
|
+
### Manual Pagination
|
|
500
|
+
|
|
501
|
+
```ruby
|
|
502
|
+
records = CardDB.records.search(dataset_key: "cards", first: 100)
|
|
503
|
+
|
|
504
|
+
while records.any?
|
|
505
|
+
records.each { |r| process(r) }
|
|
506
|
+
break unless records.next_page?
|
|
507
|
+
records = records.next_page
|
|
508
|
+
end
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Auto-Pagination (Lazy Enumerable)
|
|
512
|
+
|
|
513
|
+
```ruby
|
|
514
|
+
CardDB.records.search(dataset_key: "cards")
|
|
515
|
+
.auto_paginate
|
|
516
|
+
.take(1000)
|
|
517
|
+
.each { |record| process(record) }
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Batch Iteration (Rails-Style)
|
|
521
|
+
|
|
522
|
+
Process large datasets efficiently with `find_each` and `find_in_batches`:
|
|
523
|
+
|
|
524
|
+
```ruby
|
|
525
|
+
# Iterate one record at a time (fetches in batches behind the scenes)
|
|
526
|
+
collection = CardDB.records.search(dataset_key: "cards")
|
|
527
|
+
collection.find_each do |record|
|
|
528
|
+
puts record.name
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# Iterate in batches
|
|
532
|
+
collection.find_in_batches(batch_size: 50) do |batch|
|
|
533
|
+
batch.each { |record| process(record) }
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Returns an Enumerator when no block given
|
|
537
|
+
collection.find_each.with_index do |record, index|
|
|
538
|
+
puts "#{index}: #{record.name}"
|
|
539
|
+
end
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Collection Info
|
|
543
|
+
|
|
544
|
+
```ruby
|
|
545
|
+
records.total_count # Total matching records
|
|
546
|
+
records.size # Records in current page
|
|
547
|
+
records.next_page? # More pages available?
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
## Batch Queries
|
|
551
|
+
|
|
552
|
+
Combine multiple queries into a single API request for better performance:
|
|
553
|
+
|
|
554
|
+
```ruby
|
|
555
|
+
results = CardDB.batch do |b|
|
|
556
|
+
b.games.fetch("game-uuid-1")
|
|
557
|
+
b.games.fetch("game-uuid-2")
|
|
558
|
+
b.publishers.fetch(slug: "pokemon-company")
|
|
559
|
+
b.records.get(
|
|
560
|
+
identifier: "xy1-1",
|
|
561
|
+
dataset_key: "cards",
|
|
562
|
+
publisher_slug: "pokemon-company",
|
|
563
|
+
game_key: "pokemon-tcg"
|
|
564
|
+
)
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
results[0] # => Game
|
|
568
|
+
results[1] # => Game
|
|
569
|
+
results[2] # => Publisher
|
|
570
|
+
results[3] # => Record
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
Supported batch operations:
|
|
574
|
+
- `publishers.fetch(id:)` or `publishers.fetch(slug:)`
|
|
575
|
+
- `games.fetch(id)` or `games.get(publisher_slug:, game_key:)`
|
|
576
|
+
- `datasets.fetch(id)` or `datasets.get(publisher_slug:, game_key:, dataset_key:)`
|
|
577
|
+
- `records.fetch(id)`, `records.fetch_many(ids)`, `records.get(identifier:, dataset_key:, ...)`, or `records.get_many(identifiers:, dataset_key:, ...)`
|
|
578
|
+
|
|
579
|
+
## Caching
|
|
580
|
+
|
|
581
|
+
Enable caching to reduce API calls for repeated fetches:
|
|
582
|
+
|
|
583
|
+
### Built-in Memory Cache
|
|
584
|
+
|
|
585
|
+
```ruby
|
|
586
|
+
CardDB.configure do |config|
|
|
587
|
+
config.cache = CardDB::MemoryCache.new
|
|
588
|
+
config.cache_ttl = 300 # 5 minutes (default)
|
|
589
|
+
end
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### With Rails.cache
|
|
593
|
+
|
|
594
|
+
```ruby
|
|
595
|
+
CardDB.configure do |config|
|
|
596
|
+
config.cache = Rails.cache
|
|
597
|
+
config.cache_ttl = 300
|
|
598
|
+
end
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Per-Resource Cache TTL
|
|
602
|
+
|
|
603
|
+
Configure different TTLs for different resource types:
|
|
604
|
+
|
|
605
|
+
```ruby
|
|
606
|
+
CardDB.configure do |config|
|
|
607
|
+
config.cache = CardDB::MemoryCache.new
|
|
608
|
+
config.cache_ttl = 300 # Default: 5 minutes
|
|
609
|
+
|
|
610
|
+
# Override TTL per resource type
|
|
611
|
+
config.cache_ttls = {
|
|
612
|
+
publishers: 3600, # 1 hour (rarely change)
|
|
613
|
+
games: 3600, # 1 hour
|
|
614
|
+
datasets: 1800, # 30 minutes
|
|
615
|
+
records: 300 # 5 minutes (default)
|
|
616
|
+
}
|
|
617
|
+
end
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### Per-Request Cache Control
|
|
621
|
+
|
|
622
|
+
```ruby
|
|
623
|
+
# Enable caching for a specific call (when cache is configured)
|
|
624
|
+
game = CardDB.games.fetch("uuid", cache: true)
|
|
625
|
+
|
|
626
|
+
# Disable caching for a specific call
|
|
627
|
+
game = CardDB.games.fetch("uuid", cache: false)
|
|
628
|
+
|
|
629
|
+
# Caching is supported on fetch, get methods
|
|
630
|
+
# Search results are NOT cached (since filters vary)
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
## Logging
|
|
634
|
+
|
|
635
|
+
Enable logging to debug API interactions:
|
|
636
|
+
|
|
637
|
+
```ruby
|
|
638
|
+
CardDB.configure do |config|
|
|
639
|
+
config.logger = Logger.new(STDOUT)
|
|
640
|
+
config.log_level = :debug # :debug, :info, :warn, :error
|
|
641
|
+
end
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
Log output includes:
|
|
645
|
+
- **debug**: Query names and variables
|
|
646
|
+
- **info**: Query completion times
|
|
647
|
+
- **warn**: Rate limit retries
|
|
648
|
+
- **error**: Connection and request failures
|
|
649
|
+
|
|
650
|
+
Example output:
|
|
651
|
+
```
|
|
652
|
+
[CardDB] Executing SearchRecords with variables: {"publisherSlug"=>"pokemon-company", ...}
|
|
653
|
+
[CardDB] SearchRecords completed in 142ms
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
## Auto-Retry on Rate Limit
|
|
657
|
+
|
|
658
|
+
Automatically retry requests when rate limited:
|
|
659
|
+
|
|
660
|
+
```ruby
|
|
661
|
+
CardDB.configure do |config|
|
|
662
|
+
config.retry_on_rate_limit = true # Disabled by default
|
|
663
|
+
config.max_retries = 3 # Default: 3
|
|
664
|
+
end
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
When enabled, the client will:
|
|
668
|
+
1. Catch rate limit errors (HTTP 429)
|
|
669
|
+
2. Sleep for the `Retry-After` duration (or 60 seconds)
|
|
670
|
+
3. Retry the request up to `max_retries` times
|
|
671
|
+
4. Raise `RateLimitError` if all retries fail
|
|
672
|
+
|
|
673
|
+
## Error Handling
|
|
674
|
+
|
|
675
|
+
```ruby
|
|
676
|
+
begin
|
|
677
|
+
records = CardDB.records.search(dataset_key: "cards")
|
|
678
|
+
rescue CardDB::AuthenticationError => e
|
|
679
|
+
# Invalid API key
|
|
680
|
+
rescue CardDB::RateLimitError => e
|
|
681
|
+
# Rate limit exceeded
|
|
682
|
+
sleep(e.retry_after)
|
|
683
|
+
retry
|
|
684
|
+
rescue CardDB::RestrictedError => e
|
|
685
|
+
# Publisher/game not in allowed list
|
|
686
|
+
rescue CardDB::NotFoundError => e
|
|
687
|
+
# Resource not found
|
|
688
|
+
rescue CardDB::ValidationError => e
|
|
689
|
+
# Invalid filter or parameters
|
|
690
|
+
rescue CardDB::GraphQLError => e
|
|
691
|
+
# GraphQL-level errors
|
|
692
|
+
puts e.errors
|
|
693
|
+
rescue CardDB::ConnectionError => e
|
|
694
|
+
# Network issues
|
|
695
|
+
rescue CardDB::TimeoutError => e
|
|
696
|
+
# Request timed out
|
|
697
|
+
rescue CardDB::Error => e
|
|
698
|
+
# Base error class
|
|
699
|
+
end
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
## Rate Limits
|
|
703
|
+
|
|
704
|
+
| Authentication | Limit |
|
|
705
|
+
|----------------|-------|
|
|
706
|
+
| Anonymous | 10 requests/minute |
|
|
707
|
+
| With API Key | 100 requests/minute |
|
|
708
|
+
|
|
709
|
+
Access rate limit info after requests:
|
|
710
|
+
|
|
711
|
+
```ruby
|
|
712
|
+
records = CardDB.records.search(dataset_key: "cards")
|
|
713
|
+
info = CardDB.rate_limit_info
|
|
714
|
+
puts "Remaining: #{info[:remaining]}/#{info[:limit]}"
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
## Development
|
|
718
|
+
|
|
719
|
+
```bash
|
|
720
|
+
# Install dependencies
|
|
721
|
+
bundle install
|
|
722
|
+
|
|
723
|
+
# Run tests
|
|
724
|
+
bundle exec rspec
|
|
725
|
+
|
|
726
|
+
# Run linter
|
|
727
|
+
bundle exec rubocop
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
## License
|
|
731
|
+
|
|
732
|
+
MIT License. See [LICENSE.txt](LICENSE.txt).
|