mtgjson_sdk/lib.rs
1//! MTGJSON SDK for Rust.
2//!
3//! Provides a high-level client for querying the complete MTGJSON dataset.
4//! Data is downloaded from the MTGJSON CDN as parquet and JSON files, cached
5//! locally, and queried in-process via DuckDB.
6//!
7//! # Quick start
8//!
9//! ```no_run
10//! use mtgjson_sdk::MtgjsonSdk;
11//!
12//! let mut sdk = MtgjsonSdk::builder().build().unwrap();
13//!
14//! // Query cards
15//! let cards = sdk.cards().get_by_name("Lightning Bolt", None).unwrap();
16//!
17//! // Open a draft booster
18//! let pack = sdk.booster().open_pack("MH3", "draft").unwrap();
19//! ```
20
21#[cfg(feature = "async")]
22pub mod async_client;
23pub mod booster;
24pub mod cache;
25pub mod config;
26pub mod connection;
27pub mod error;
28pub mod models;
29pub mod queries;
30pub mod sql_builder;
31
32#[cfg(feature = "async")]
33pub use async_client::AsyncMtgjsonSdk;
34pub use cache::CacheManager;
35pub use connection::Connection;
36pub use error::{MtgjsonError, Result};
37pub use sql_builder::SqlBuilder;
38
39use std::collections::HashMap;
40use std::fmt;
41use std::path::{Path, PathBuf};
42use std::sync::Arc;
43use std::time::Duration;
44
45/// Callback for download progress reporting.
46///
47/// Arguments: `(filename, bytes_downloaded, total_bytes)`.
48/// `total_bytes` may be `0` if the server did not provide a `Content-Length` header.
49pub type ProgressCallback = Arc<dyn Fn(&str, u64, u64) + Send + Sync>;
50
51// ---------------------------------------------------------------------------
52// MtgjsonSdkBuilder
53// ---------------------------------------------------------------------------
54
55/// Builder for configuring and constructing an [`MtgjsonSdk`] instance.
56///
57/// Use [`MtgjsonSdk::builder()`] to obtain a builder, chain configuration
58/// methods, and call [`build()`](MtgjsonSdkBuilder::build) to create the SDK.
59pub struct MtgjsonSdkBuilder {
60 cache_dir: Option<PathBuf>,
61 offline: bool,
62 timeout: Duration,
63 on_progress: Option<ProgressCallback>,
64}
65
66impl Default for MtgjsonSdkBuilder {
67 fn default() -> Self {
68 Self {
69 cache_dir: None,
70 offline: false,
71 timeout: Duration::from_secs(120),
72 on_progress: None,
73 }
74 }
75}
76
77impl MtgjsonSdkBuilder {
78 /// Set a custom cache directory.
79 ///
80 /// If not set, the platform-appropriate default cache directory is used
81 /// (e.g. `~/.cache/mtgjson-sdk` on Linux, `~/Library/Caches/mtgjson-sdk`
82 /// on macOS, `%LOCALAPPDATA%\mtgjson-sdk` on Windows).
83 pub fn cache_dir<P: AsRef<Path>>(mut self, path: P) -> Self {
84 self.cache_dir = Some(path.as_ref().to_path_buf());
85 self
86 }
87
88 /// Enable or disable offline mode.
89 ///
90 /// When offline, the SDK never downloads from the CDN and only uses
91 /// previously cached data files. Defaults to `false`.
92 pub fn offline(mut self, offline: bool) -> Self {
93 self.offline = offline;
94 self
95 }
96
97 /// Set the HTTP request timeout for CDN downloads.
98 ///
99 /// Defaults to 120 seconds.
100 pub fn timeout(mut self, timeout: Duration) -> Self {
101 self.timeout = timeout;
102 self
103 }
104
105 /// Set a progress callback for CDN downloads.
106 ///
107 /// The callback receives `(filename, bytes_downloaded, total_bytes)` and is
108 /// called periodically during file downloads. `total_bytes` is `0` if the
109 /// server did not provide a `Content-Length` header.
110 pub fn on_progress<F>(mut self, f: F) -> Self
111 where
112 F: Fn(&str, u64, u64) + Send + Sync + 'static,
113 {
114 self.on_progress = Some(Arc::new(f));
115 self
116 }
117
118 /// Build the SDK, initializing the cache and DuckDB connection.
119 ///
120 /// This may trigger a version check against the CDN (unless offline mode
121 /// is enabled) but does **not** download any data files eagerly -- they
122 /// are fetched lazily on first query.
123 pub fn build(self) -> Result<MtgjsonSdk> {
124 let cache = CacheManager::new(
125 self.cache_dir,
126 self.offline,
127 self.timeout,
128 self.on_progress,
129 )?;
130 let conn = Connection::new(cache)?;
131 Ok(MtgjsonSdk { conn })
132 }
133}
134
135// ---------------------------------------------------------------------------
136// MtgjsonSdk
137// ---------------------------------------------------------------------------
138
139/// The main entry point for the MTGJSON SDK.
140///
141/// Wraps a [`Connection`] (which owns the [`CacheManager`] and DuckDB database)
142/// and exposes domain-specific query interfaces as lightweight borrowing wrappers.
143///
144/// Created via [`MtgjsonSdk::builder()`].
145pub struct MtgjsonSdk {
146 conn: Connection,
147}
148
149impl MtgjsonSdk {
150 /// Create a new builder for configuring the SDK.
151 pub fn builder() -> MtgjsonSdkBuilder {
152 MtgjsonSdkBuilder::default()
153 }
154
155 // -- Query accessors ---------------------------------------------------
156
157 /// Access the card query interface.
158 ///
159 /// Returns a lightweight wrapper that borrows from the underlying
160 /// connection and provides methods for querying card data.
161 pub fn cards(&self) -> queries::cards::CardQuery<'_> {
162 queries::cards::CardQuery::new(&self.conn)
163 }
164
165 /// Access the set query interface.
166 pub fn sets(&self) -> queries::sets::SetQuery<'_> {
167 queries::sets::SetQuery::new(&self.conn)
168 }
169
170 /// Access the token query interface.
171 pub fn tokens(&self) -> queries::tokens::TokenQuery<'_> {
172 queries::tokens::TokenQuery::new(&self.conn)
173 }
174
175 /// Access the price query interface.
176 ///
177 /// Requires the `prices_today` table to have been loaded into DuckDB.
178 pub fn prices(&self) -> queries::prices::PriceQuery<'_> {
179 queries::prices::PriceQuery::new(&self.conn)
180 }
181
182 /// Access the legality query interface.
183 pub fn legalities(&self) -> queries::legalities::LegalityQuery<'_> {
184 queries::legalities::LegalityQuery::new(&self.conn)
185 }
186
187 /// Access the identifier query interface.
188 pub fn identifiers(&self) -> queries::identifiers::IdentifierQuery<'_> {
189 queries::identifiers::IdentifierQuery::new(&self.conn)
190 }
191
192 /// Access the deck query interface.
193 ///
194 /// Deck data is loaded from `DeckList.json` via the cache manager.
195 pub fn decks(&self) -> queries::decks::DeckQuery<'_> {
196 queries::decks::DeckQuery::new(&self.conn)
197 }
198
199 /// Access the sealed product query interface.
200 pub fn sealed(&self) -> queries::sealed::SealedQuery<'_> {
201 queries::sealed::SealedQuery::new(&self.conn)
202 }
203
204 /// Access the TCGplayer SKU query interface.
205 ///
206 /// Requires the `tcgplayer_skus` table to have been loaded into DuckDB.
207 pub fn skus(&self) -> queries::skus::SkuQuery<'_> {
208 queries::skus::SkuQuery::new(&self.conn)
209 }
210
211 /// Access the enum/keyword query interface.
212 ///
213 /// Enum data is loaded from JSON files (`Keywords.json`, `CardTypes.json`,
214 /// `EnumValues.json`) via the cache manager.
215 pub fn enums(&self) -> queries::enums::EnumQuery<'_> {
216 queries::enums::EnumQuery::new(&self.conn)
217 }
218
219 /// Access the booster pack simulator.
220 ///
221 /// The simulator reads from the set booster parquet tables to generate
222 /// randomized booster packs matching real-world distribution rules.
223 pub fn booster(&self) -> booster::BoosterSimulator<'_> {
224 booster::BoosterSimulator::new(&self.conn)
225 }
226
227 // -- Metadata and utility methods --------------------------------------
228
229 /// Load and return the MTGJSON metadata (version, date, etc.).
230 ///
231 /// Fetches `Meta.json` from the cache (downloading if necessary) and
232 /// returns the parsed JSON object.
233 pub fn meta(&self) -> Result<serde_json::Value> {
234 self.conn.cache.borrow_mut().load_json("meta")
235 }
236
237 /// Return the list of currently registered DuckDB view names.
238 ///
239 /// Views are registered lazily on first query, so this list grows as
240 /// different query interfaces are used.
241 pub fn views(&self) -> Vec<String> {
242 self.conn.views()
243 }
244
245 /// Execute a raw SQL query against the DuckDB database.
246 ///
247 /// Provides escape-hatch access for queries not covered by the
248 /// domain-specific interfaces.
249 ///
250 /// # Arguments
251 ///
252 /// * `query` - SQL string with `?` positional placeholders.
253 /// * `params` - Parameter values corresponding to the placeholders.
254 ///
255 /// # Returns
256 ///
257 /// A vector of rows, each represented as a `HashMap<String, serde_json::Value>`.
258 pub fn sql(
259 &self,
260 query: &str,
261 params: &[String],
262 ) -> Result<Vec<HashMap<String, serde_json::Value>>> {
263 self.conn.execute(query, params)
264 }
265
266 /// Export the in-memory DuckDB database to a directory on disk.
267 ///
268 /// Uses DuckDB's `EXPORT DATABASE` command to write the database contents
269 /// (schema + data) to the given path.
270 pub fn export_db<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
271 self.conn.export_db(path.as_ref())
272 }
273
274 /// Execute a raw SQL query and return the result as a Polars DataFrame.
275 ///
276 /// This is the Rust equivalent of Python's `sdk.sql("...", as_dataframe=True)`.
277 /// Requires the `polars` cargo feature.
278 #[cfg(feature = "polars")]
279 pub fn sql_df(
280 &self,
281 query: &str,
282 params: &[String],
283 ) -> Result<polars::frame::DataFrame> {
284 self.conn.execute_df(query, params)
285 }
286
287 /// Check for a newer MTGJSON version and reset views if stale.
288 ///
289 /// Returns `true` if the data was stale and views were reset (meaning
290 /// subsequent queries will re-download data), or `false` if already
291 /// up to date.
292 pub fn refresh(&self) -> Result<bool> {
293 let stale = self.conn.cache.borrow_mut().is_stale()?;
294 if stale {
295 self.conn.cache.borrow().clear()?;
296 self.conn.reset_views();
297 eprintln!("MTGJSON data was stale; cache cleared and views reset");
298 }
299 Ok(stale)
300 }
301
302 /// Consume the SDK and release all resources.
303 ///
304 /// Closes the DuckDB connection and HTTP client. This is called
305 /// automatically when the SDK is dropped, but can be invoked explicitly
306 /// for deterministic cleanup.
307 pub fn close(self) {
308 // Connection and CacheManager are dropped automatically
309 drop(self);
310 }
311
312 /// Return a reference to the underlying [`Connection`] for advanced usage.
313 pub fn connection(&self) -> &Connection {
314 &self.conn
315 }
316
317 /// Return a mutable reference to the underlying [`Connection`].
318 pub fn connection_mut(&mut self) -> &mut Connection {
319 &mut self.conn
320 }
321}
322
323// ---------------------------------------------------------------------------
324// Display
325// ---------------------------------------------------------------------------
326
327impl fmt::Display for MtgjsonSdk {
328 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329 let views = self.conn.views();
330 let cache = self.conn.cache.borrow();
331 write!(
332 f,
333 "MtgjsonSdk(cache_dir={}, views=[{}], offline={})",
334 cache.cache_dir.display(),
335 views.join(", "),
336 cache.offline
337 )
338 }
339}