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.
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CardDB
4
+ module Resources
5
+ # Base class for API resources
6
+ class Base
7
+ # @return [Client] The client instance
8
+ attr_reader :client
9
+
10
+ # @return [Connection] The API connection
11
+ attr_reader :connection
12
+
13
+ # @return [Configuration] The configuration
14
+ attr_reader :config
15
+
16
+ # @param client [Client] The client instance
17
+ # @param connection [Connection] The API connection
18
+ # @param config [Configuration] The configuration
19
+ def initialize(client, connection, config)
20
+ @client = client
21
+ @connection = connection
22
+ @config = config
23
+ end
24
+
25
+ private
26
+
27
+ # Resolve publisher slug, using default if not provided
28
+ def resolve_publisher(publisher_slug)
29
+ resolved = config.resolve_publisher(publisher_slug)
30
+ return resolved if resolved
31
+
32
+ raise ArgumentError, 'publisher_slug is required (no default configured)'
33
+ end
34
+
35
+ # Resolve game key, using default if not provided
36
+ def resolve_game(game_key)
37
+ resolved = config.resolve_game(game_key)
38
+ return resolved if resolved
39
+
40
+ raise ArgumentError, 'game_key is required (no default configured)'
41
+ end
42
+
43
+ # Validate access to publisher/game
44
+ def validate_access!(publisher_slug, game_key = nil)
45
+ config.validate_access!(publisher_slug, game_key)
46
+ end
47
+
48
+ # Build variables hash, removing nil values
49
+ def build_variables(**vars)
50
+ vars.compact
51
+ end
52
+
53
+ # Execute a query with optional caching.
54
+ #
55
+ # @param cache_key [String] The cache key
56
+ # @param resource [Symbol] The resource type for TTL lookup (:publishers, :games, :datasets, :records)
57
+ # @param cache [Boolean, nil] Whether to use cache (nil = use config default)
58
+ # @yield Block that executes the query and returns the result
59
+ # @return [Object] The result (from cache or fresh)
60
+ def with_cache(cache_key, resource:, cache: nil)
61
+ use_cache = cache.nil? ? config.cache : cache
62
+ return yield unless use_cache && config.cache
63
+
64
+ # Try to read from cache
65
+ cached = config.cache.read(cache_key)
66
+ return cached if cached
67
+
68
+ # Execute query and cache result with resource-specific TTL
69
+ result = yield
70
+ ttl = config.cache_ttl_for(resource)
71
+ config.cache.write(cache_key, result, expires_in: ttl) if result
72
+ result
73
+ end
74
+
75
+ # Generate a cache key for a resource operation.
76
+ #
77
+ # @param resource [String] Resource type
78
+ # @param method [String] Method name
79
+ # @param params [Hash] Parameters
80
+ # @return [String] The cache key
81
+ def cache_key(resource, method, **params)
82
+ CacheSupport.cache_key(resource, method, params)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CardDB
4
+ module Resources
5
+ # Datasets resource for searching, fetching, and getting schema
6
+ class Datasets < Base
7
+ # Search for datasets
8
+ #
9
+ # @param publisher_slug [String, nil] Filter by publisher slug
10
+ # @param game_key [String, nil] Filter by game key
11
+ # @param search [String, nil] Search by name
12
+ # @param purpose [String, nil] Filter by dataset purpose (DATA or RULES)
13
+ # @param first [Integer, nil] Maximum number of results
14
+ # @param after [String, nil] Cursor for pagination
15
+ # @return [Collection<Dataset>] Collection of datasets
16
+ def search(publisher_slug: nil, game_key: nil, search: nil, purpose: nil, first: nil, after: nil)
17
+ resolved_publisher = config.resolve_publisher(publisher_slug)
18
+ resolved_game = config.resolve_game(game_key)
19
+
20
+ validate_access!(resolved_publisher, resolved_game) if resolved_publisher
21
+
22
+ query = QueryBuilder.search_datasets(
23
+ publisher_slug: resolved_publisher,
24
+ game_key: resolved_game,
25
+ search: search,
26
+ purpose: purpose,
27
+ first: first,
28
+ after: after
29
+ )
30
+
31
+ variables = build_variables(
32
+ publisherSlug: resolved_publisher,
33
+ gameKey: resolved_game,
34
+ search: search,
35
+ purpose: purpose,
36
+ first: first,
37
+ after: after
38
+ )
39
+
40
+ data = connection.execute(query, variables)
41
+
42
+ # Create next page loader
43
+ search_params = { publisher_slug: publisher_slug, game_key: game_key, search: search, purpose: purpose, first: first }
44
+ next_page_loader = ->(cursor) { search(**search_params, after: cursor) }
45
+
46
+ Collection.new(
47
+ data['searchDatasets'],
48
+ item_class: Dataset,
49
+ next_page_loader: next_page_loader,
50
+ client: client
51
+ )
52
+ end
53
+
54
+ # Fetch a dataset by ID (includes schema)
55
+ #
56
+ # @param id [String] The dataset UUID
57
+ # @param cache [Boolean, nil] Whether to cache (nil = use config setting)
58
+ # @return [Dataset, nil] The dataset or nil if not found
59
+ def fetch(id, cache: nil)
60
+ key = cache_key('datasets', 'fetch', id: id)
61
+ with_cache(key, resource: :datasets, cache: cache) do
62
+ query = QueryBuilder.fetch_dataset_by_id
63
+ data = connection.execute(query, { id: id })
64
+
65
+ return nil unless data['fetchDataset']
66
+
67
+ Dataset.new(data['fetchDataset'], client: client)
68
+ end
69
+ end
70
+
71
+ # Get a dataset by publisher/game/dataset keys (includes schema)
72
+ #
73
+ # @param publisher_slug [String, nil] Publisher slug (uses default if not provided)
74
+ # @param game_key [String, nil] Game key (uses default if not provided)
75
+ # @param dataset_key [String] Dataset key (required)
76
+ # @param cache [Boolean, nil] Whether to cache (nil = use config setting)
77
+ # @return [Dataset, nil] The dataset or nil if not found
78
+ def get(dataset_key:, publisher_slug: nil, game_key: nil, cache: nil)
79
+ resolved_publisher = resolve_publisher(publisher_slug)
80
+ resolved_game = resolve_game(game_key)
81
+
82
+ validate_access!(resolved_publisher, resolved_game)
83
+
84
+ key = cache_key('datasets', 'get', publisher_slug: resolved_publisher, game_key: resolved_game,
85
+ dataset_key: dataset_key)
86
+ with_cache(key, resource: :datasets, cache: cache) do
87
+ query = QueryBuilder.fetch_dataset_by_keys
88
+ variables = {
89
+ publisherSlug: resolved_publisher,
90
+ gameKey: resolved_game,
91
+ datasetKey: dataset_key
92
+ }
93
+
94
+ data = connection.execute(query, variables)
95
+
96
+ return nil unless data['fetchDataset']
97
+
98
+ Dataset.new(data['fetchDataset'], client: client)
99
+ end
100
+ end
101
+
102
+ # Fetch multiple datasets by IDs
103
+ #
104
+ # @param ids [Array<String>] Array of dataset UUIDs (max 100)
105
+ # @return [Array<Dataset>] Array of datasets
106
+ # @raise [ArgumentError] If more than 100 IDs provided
107
+ def fetch_many(ids)
108
+ raise ArgumentError, 'Maximum 100 IDs allowed' if ids.length > 100
109
+
110
+ query = QueryBuilder.fetch_datasets
111
+ data = connection.execute(query, { ids: ids })
112
+
113
+ (data['fetchDatasets'] || []).map { |d| Dataset.new(d, client: client) }
114
+ end
115
+
116
+ # Get the schema for a dataset
117
+ #
118
+ # @param dataset_key [String] Dataset key
119
+ # @param publisher_slug [String, nil] Publisher slug (uses default if not provided)
120
+ # @param game_key [String, nil] Game key (uses default if not provided)
121
+ # @return [DatasetSchema, nil] The schema or nil if not found
122
+ def schema(dataset_key:, publisher_slug: nil, game_key: nil)
123
+ dataset = get(
124
+ dataset_key: dataset_key,
125
+ publisher_slug: publisher_slug,
126
+ game_key: game_key
127
+ )
128
+ dataset&.schema
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CardDB
4
+ module Resources
5
+ # Decks resource for hosted decks and external deck hydration.
6
+ class Decks < Base
7
+ # List decks owned by the current account or API application.
8
+ def list_mine(first: nil, after: nil, cache: nil)
9
+ query = QueryBuilder.list_my_decks(first: first, after: after)
10
+ variables = build_variables(first: first, after: after)
11
+ key = cache_key('decks', 'list_mine', **variables)
12
+
13
+ data = with_cache(key, resource: :decks, cache: cache) do
14
+ connection.execute(query, variables)
15
+ end
16
+
17
+ Collection.new(
18
+ data['myDecks'],
19
+ item_class: Deck,
20
+ next_page_loader: ->(cursor) { list_mine(first: first, after: cursor, cache: cache) },
21
+ client: client
22
+ )
23
+ end
24
+
25
+ # Fetch a hosted deck by CardDB UUID.
26
+ def fetch(id, cache: nil)
27
+ key = cache_key('decks', 'fetch', id: id)
28
+ with_cache(key, resource: :decks, cache: cache) do
29
+ data = connection.execute(QueryBuilder.fetch_deck, { id: id })
30
+ data['fetchDeck'] ? Deck.new(data['fetchDeck'], client: client) : nil
31
+ end
32
+ end
33
+
34
+ # Fetch a hosted deck by external reference for the current API application.
35
+ def fetch_by_external_ref(external_ref:, cache: nil)
36
+ key = cache_key('decks', 'fetch_by_external_ref', external_ref: external_ref)
37
+ with_cache(key, resource: :decks, cache: cache) do
38
+ data = connection.execute(QueryBuilder.fetch_deck_by_external_ref, { externalRef: external_ref })
39
+ data['fetchDeckByExternalRef'] ? Deck.new(data['fetchDeckByExternalRef'], client: client) : nil
40
+ end
41
+ end
42
+
43
+ # Create a hosted deck.
44
+ def create(input:)
45
+ data = connection.execute(QueryBuilder.create_deck, { input: input })
46
+ Deck.new(data['deckCreate'], client: client)
47
+ end
48
+
49
+ # Update a hosted deck.
50
+ def update(id:, input:)
51
+ data = connection.execute(QueryBuilder.update_deck, { id: id, input: input })
52
+ Deck.new(data['deckUpdate'], client: client)
53
+ end
54
+
55
+ # Delete a hosted deck.
56
+ # rubocop:disable Naming/PredicateMethod
57
+ def delete(id:)
58
+ data = connection.execute(QueryBuilder.delete_deck, { id: id })
59
+ !!data['deckDelete']
60
+ end
61
+ # rubocop:enable Naming/PredicateMethod
62
+
63
+ # Hydrate third-party-owned deck entries without storing them in CardDB.
64
+ def hydrate_entries(dataset_key:, entries:, publisher_slug: nil, game_key: nil, identifier_field: nil, cache: nil)
65
+ return [] if entries.empty?
66
+
67
+ resolved_publisher = resolve_publisher(publisher_slug)
68
+ resolved_game = resolve_game(game_key)
69
+ validate_access!(resolved_publisher, resolved_game)
70
+
71
+ identifiers = entries.map { |entry| entry_identifier(entry) }
72
+ key = cache_key(
73
+ 'decks',
74
+ 'hydrate_entries',
75
+ publisher_slug: resolved_publisher,
76
+ game_key: resolved_game,
77
+ dataset_key: dataset_key,
78
+ identifiers: identifiers
79
+ )
80
+ records = with_cache(key, resource: :decks, cache: cache) do
81
+ data = connection.execute(
82
+ QueryBuilder.fetch_records_by_identifier,
83
+ {
84
+ publisherSlug: resolved_publisher,
85
+ gameKey: resolved_game,
86
+ datasetKey: dataset_key,
87
+ identifiers: identifiers
88
+ }
89
+ )
90
+ (data['fetchRecordsByIdentifier'] || []).map { |record| Record.new(record, client: client) }
91
+ end
92
+
93
+ records_by_identifier = records_by_deck_identifier(records, identifiers, identifier_field)
94
+ entries.map do |entry|
95
+ entry.merge(record: records_by_identifier[entry_identifier(entry)])
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def entry_identifier(entry)
102
+ entry[:identifier] || entry['identifier'] || raise(ArgumentError, 'entry identifier is required')
103
+ end
104
+
105
+ def records_by_deck_identifier(records, identifiers, identifier_field)
106
+ remaining = identifiers.to_h { |identifier| [identifier.to_s, true] }
107
+ records.each_with_object({}) do |record, hash|
108
+ identifier = deck_record_identifier(record, remaining, identifier_field)
109
+ next unless identifier
110
+
111
+ key = identifier.to_s
112
+ next unless remaining.delete(key)
113
+
114
+ hash[key] = record
115
+ end
116
+ end
117
+
118
+ def deck_record_identifier(record, remaining, identifier_field)
119
+ return record[identifier_field] if identifier_field
120
+
121
+ record.record_data&.values&.find { |value| remaining[value.to_s] }
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CardDB
4
+ module Resources
5
+ # Games resource for searching and fetching games
6
+ class Games < Base
7
+ # Search for games
8
+ #
9
+ # @param publisher_slug [String, nil] Filter by publisher slug
10
+ # @param search [String, nil] Search by name
11
+ # @param first [Integer, nil] Maximum number of results
12
+ # @param after [String, nil] Cursor for pagination
13
+ # @return [Collection<Game>] Collection of games
14
+ def search(publisher_slug: nil, search: nil, first: nil, after: nil)
15
+ resolved_publisher = config.resolve_publisher(publisher_slug)
16
+ validate_access!(resolved_publisher, nil) if resolved_publisher
17
+
18
+ query = QueryBuilder.search_games(
19
+ publisher_slug: resolved_publisher,
20
+ search: search,
21
+ first: first,
22
+ after: after
23
+ )
24
+
25
+ variables = build_variables(
26
+ publisherSlug: resolved_publisher,
27
+ search: search,
28
+ first: first,
29
+ after: after
30
+ )
31
+
32
+ data = connection.execute(query, variables)
33
+
34
+ # Create next page loader
35
+ next_page_loader = lambda do |cursor|
36
+ search(
37
+ publisher_slug: publisher_slug,
38
+ search: search,
39
+ first: first,
40
+ after: cursor
41
+ )
42
+ end
43
+
44
+ Collection.new(
45
+ data['searchGames'],
46
+ item_class: Game,
47
+ next_page_loader: next_page_loader,
48
+ client: client
49
+ )
50
+ end
51
+
52
+ # Fetch a game by ID
53
+ #
54
+ # @param id [String] The game UUID
55
+ # @param cache [Boolean, nil] Whether to cache (nil = use config setting)
56
+ # @return [Game, nil] The game or nil if not found
57
+ def fetch(id, cache: nil)
58
+ key = cache_key('games', 'fetch', id: id)
59
+ with_cache(key, resource: :games, cache: cache) do
60
+ query = QueryBuilder.fetch_game_by_id
61
+ data = connection.execute(query, { id: id })
62
+
63
+ return nil unless data['fetchGame']
64
+
65
+ Game.new(data['fetchGame'], client: client)
66
+ end
67
+ end
68
+
69
+ # Get a game by publisher slug and game key
70
+ #
71
+ # @param game_key [String] Game key (required)
72
+ # @param publisher_slug [String, nil] Publisher slug (uses default if not provided)
73
+ # @param cache [Boolean, nil] Whether to cache (nil = use config setting)
74
+ # @return [Game, nil] The game or nil if not found
75
+ def get(game_key:, publisher_slug: nil, cache: nil)
76
+ resolved_publisher = resolve_publisher(publisher_slug)
77
+
78
+ validate_access!(resolved_publisher, game_key)
79
+
80
+ key = cache_key('games', 'get', publisher_slug: resolved_publisher, game_key: game_key)
81
+ with_cache(key, resource: :games, cache: cache) do
82
+ query = QueryBuilder.fetch_game_by_keys
83
+ variables = {
84
+ publisherSlug: resolved_publisher,
85
+ gameKey: game_key
86
+ }
87
+
88
+ data = connection.execute(query, variables)
89
+
90
+ return nil unless data['fetchGame']
91
+
92
+ Game.new(data['fetchGame'], client: client)
93
+ end
94
+ end
95
+
96
+ # Fetch multiple games by IDs
97
+ #
98
+ # @param ids [Array<String>] Array of game UUIDs (max 100)
99
+ # @return [Array<Game>] Array of games
100
+ # @raise [ArgumentError] If more than 100 IDs provided
101
+ def fetch_many(ids)
102
+ raise ArgumentError, 'Maximum 100 IDs allowed' if ids.length > 100
103
+
104
+ query = QueryBuilder.fetch_games
105
+ data = connection.execute(query, { ids: ids })
106
+
107
+ (data['fetchGames'] || []).map { |g| Game.new(g, client: client) }
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CardDB
4
+ module Resources
5
+ # Publishers resource for searching and fetching publishers
6
+ class Publishers < Base
7
+ # Search for publishers
8
+ #
9
+ # @param search [String, nil] Search by name
10
+ # @param first [Integer, nil] Maximum number of results
11
+ # @param after [String, nil] Cursor for pagination
12
+ # @return [Collection<Publisher>] Collection of publishers
13
+ def search(search: nil, first: nil, after: nil)
14
+ query = QueryBuilder.search_publishers(
15
+ search: search,
16
+ first: first,
17
+ after: after
18
+ )
19
+
20
+ variables = build_variables(
21
+ search: search,
22
+ first: first,
23
+ after: after
24
+ )
25
+
26
+ data = connection.execute(query, variables)
27
+
28
+ # Create next page loader
29
+ next_page_loader = lambda do |cursor|
30
+ search(
31
+ search: search,
32
+ first: first,
33
+ after: cursor
34
+ )
35
+ end
36
+
37
+ Collection.new(
38
+ data['searchPublishers'],
39
+ item_class: Publisher,
40
+ next_page_loader: next_page_loader,
41
+ client: client
42
+ )
43
+ end
44
+
45
+ # Fetch a publisher by ID or slug
46
+ #
47
+ # @param id [String, nil] The publisher UUID
48
+ # @param slug [String, nil] The publisher slug
49
+ # @param cache [Boolean, nil] Whether to cache (nil = use config setting)
50
+ # @return [Publisher, nil] The publisher or nil if not found
51
+ # @raise [ArgumentError] If neither id nor slug is provided
52
+ def fetch(id: nil, slug: nil, cache: nil)
53
+ raise ArgumentError, 'Must provide either id or slug' if id.nil? && slug.nil?
54
+
55
+ key = cache_key('publishers', 'fetch', id: id, slug: slug)
56
+ with_cache(key, resource: :publishers, cache: cache) do
57
+ if id
58
+ query = QueryBuilder.fetch_publisher_by_id
59
+ data = connection.execute(query, { id: id })
60
+ else
61
+ query = QueryBuilder.fetch_publisher_by_slug
62
+ data = connection.execute(query, { slug: slug })
63
+ end
64
+
65
+ return nil unless data['fetchPublisher']
66
+
67
+ Publisher.new(data['fetchPublisher'], client: client)
68
+ end
69
+ end
70
+
71
+ # Fetch multiple publishers by slugs
72
+ #
73
+ # @param slugs [Array<String>] Array of publisher slugs (max 100)
74
+ # @return [Array<Publisher>] Array of publishers
75
+ # @raise [ArgumentError] If more than 100 slugs provided
76
+ def fetch_many(slugs)
77
+ raise ArgumentError, 'Maximum 100 slugs allowed' if slugs.length > 100
78
+
79
+ query = QueryBuilder.fetch_publishers
80
+ data = connection.execute(query, { slugs: slugs })
81
+
82
+ (data['fetchPublishers'] || []).map { |p| Publisher.new(p, client: client) }
83
+ end
84
+ end
85
+ end
86
+ end