Skip to main content

dtrpg_sdk/library/
models.rs

1//! Library resource types for the DriveThruRPG SDK.
2//!
3//! This module provides Rust model types that mirror the API-defined schemas for library
4//! resources: ordered products, product files, product lists, and associated pagination
5//! and metadata structures.
6//!
7//! All response types implement [`serde::Deserialize`] (and [`serde::Serialize`]) so they
8//! can be decoded directly from JSON returned by the DriveThruRPG API. Field names follow
9//! the API contract and are mapped from camelCase JSON to snake_case Rust conventions via
10//! `#[serde(rename = "...")]` attributes.
11//!
12//! Query parameter structs ([`LibraryItemsParams`], [`PageParams`]) are plain Rust structs
13//! with no serde requirement; they are consumed by [`LibraryClient`] methods to build
14//! URL query strings.
15//!
16//! [`LibraryClient`]: crate::LibraryClient
17
18use serde::{Deserialize, Deserializer, Serialize};
19
20/// Deserializes a JSON null or missing field as the type's default value.
21///
22/// Use via `#[serde(default, deserialize_with = "null_as_default")]` on fields that the
23/// API may send as `null` but that should be treated as empty collections or zero values.
24fn null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
25where
26    T: Default + Deserialize<'de>,
27    D: Deserializer<'de>,
28{
29    Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
30}
31
32// ── Pagination ────────────────────────────────────────────────────────────────
33
34/// Pagination links included in all paginated API responses.
35///
36/// The `self_` field corresponds to the JSON key `"self"`, which is a Rust reserved
37/// keyword and is therefore renamed at the struct level via `#[serde(rename = "self")]`.
38#[derive(Clone, Debug, Deserialize, Serialize)]
39pub struct PaginationLinks {
40    /// The canonical URL for the current page of results.
41    #[serde(rename = "self")]
42    pub self_: String,
43    /// URL for the first page of results, if available.
44    pub first: Option<String>,
45    /// URL for the last page of results, if available.
46    pub last: Option<String>,
47    /// URL for the previous page of results, if available.
48    pub prev: Option<String>,
49    /// URL for the next page of results, if available.
50    pub next: Option<String>,
51}
52
53/// Pagination metadata included in all paginated API responses.
54#[derive(Clone, Debug, Deserialize, Serialize)]
55pub struct PaginationMeta {
56    /// The number of items returned per page.
57    #[serde(rename = "itemsPerPage")]
58    pub items_per_page: u32,
59    /// The current page number (1-based).
60    #[serde(rename = "currentPage")]
61    pub current_page: u32,
62}
63
64// ── File / Checksum ───────────────────────────────────────────────────────────
65
66/// Checksum information for a single downloadable product file.
67#[derive(Clone, Debug, Deserialize, Serialize)]
68pub struct FileChecksum {
69    /// The checksum hash string for the file.
70    pub checksum: String,
71    /// The date when the checksum was generated (ISO 8601 string).
72    #[serde(rename = "checksumDate")]
73    pub checksum_date: String,
74}
75
76/// A downloadable file associated with an ordered product.
77#[derive(Clone, Debug, Deserialize, Serialize)]
78pub struct OrderProductFile {
79    /// The index of this file within the ordered product's file list.
80    pub index: u32,
81    /// The unique identifier for this specific download record.
82    #[serde(rename = "orderProductDownloadId")]
83    pub order_product_download_id: u64,
84    /// The display title of the file.
85    pub title: String,
86    /// The filename as it will appear when downloaded.
87    pub filename: String,
88    /// The file size in bytes.
89    pub size: u64,
90    /// The file size expressed in megabytes as a formatted string.
91    #[serde(rename = "sizeMB")]
92    pub size_mb: String,
93    /// Checksums available for verifying the integrity of the downloaded file.
94    ///
95    /// The API may return `null` for products without checksum data; treated as empty.
96    #[serde(default, deserialize_with = "null_as_default")]
97    pub checksums: Vec<FileChecksum>,
98}
99
100// ── Filters / History / Attributes ───────────────────────────────────────────
101
102/// A filter category associated with an ordered product.
103///
104/// Populated when `getFilters=1` is included in the request.
105#[derive(Clone, Debug, Deserialize, Serialize)]
106pub struct OrderProductFilter {
107    /// The unique identifier of this filter category.
108    #[serde(rename = "filterId")]
109    pub filter_id: u64,
110    /// The unique identifier of this filter's parent category.
111    #[serde(rename = "parentFilterId")]
112    pub parent_filter_id: u64,
113    /// The display name of this filter category.
114    pub name: String,
115    /// The display name of this filter's parent category.
116    #[serde(rename = "parentName")]
117    pub parent_name: String,
118}
119
120/// A single history entry recording a change made to an ordered product.
121#[derive(Clone, Debug, Deserialize, Serialize)]
122pub struct OrderProductHistoryEntry {
123    /// The date and time when the change occurred (ISO 8601 string).
124    pub changed: String,
125    /// A human-readable description of what changed.
126    pub changes: String,
127}
128
129/// An individual attribute option associated with an ordered product.
130///
131/// Attributes describe purchase options such as format or edition.
132#[derive(Clone, Debug, Deserialize, Serialize)]
133pub struct OrderProductAttribute {
134    /// The unique identifier of the order this attribute belongs to.
135    #[serde(rename = "orderId")]
136    pub order_id: u64,
137    /// The name of the option (e.g., `"Format"`).
138    #[serde(rename = "optionName")]
139    pub option_name: String,
140    /// The display name of the selected option value (e.g., `"PDF"`).
141    #[serde(rename = "optionValueName")]
142    pub option_value_name: String,
143    /// The price associated with this option, as a formatted string.
144    pub price: String,
145    /// A prefix to display before the price (e.g., `"$"`).
146    #[serde(rename = "pricePrefix")]
147    pub price_prefix: String,
148    /// The unique identifier for the selected option value.
149    #[serde(rename = "optionValueId")]
150    pub option_value_id: u64,
151    /// The type classification of this option.
152    #[serde(rename = "optionType")]
153    pub option_type: String,
154}
155
156// ── OrderProduct ──────────────────────────────────────────────────────────────
157
158/// The full attribute set for an ordered product.
159///
160/// This is the primary payload within an [`OrderProductItem`]. It includes required fields
161/// present on every ordered product as well as optional collections (filters, history,
162/// attributes) that are populated only when specifically requested.
163#[derive(Clone, Debug, Deserialize, Serialize)]
164pub struct OrderProductAttributes {
165    /// The unique identifier of the order this product belongs to.
166    #[serde(rename = "orderId")]
167    pub order_id: u64,
168    /// The unique identifier of the product.
169    #[serde(rename = "productId")]
170    pub product_id: u64,
171    /// The publisher identifier used for royalty tracking.
172    #[serde(rename = "royaltyPublisherId")]
173    pub royalty_publisher_id: u64,
174    /// The ISBN of the product, if applicable.
175    pub isbn: Option<String>,
176    /// The display name of the product.
177    pub name: String,
178    /// The date the product was purchased (ISO 8601 string), if available.
179    #[serde(rename = "datePurchased")]
180    pub date_purchased: Option<String>,
181    /// The total file size in bytes, if available.
182    pub filesize: Option<u64>,
183    /// The final price paid for the product.
184    #[serde(rename = "finalPrice")]
185    pub final_price: f64,
186    /// The quantity of this product in the order.
187    pub quantity: u32,
188    /// The bundle identifier, if the product was purchased as part of a bundle.
189    #[serde(rename = "bundleId")]
190    pub bundle_id: u64,
191    /// Indicates whether the product has been archived (`1`) or not (`0`).
192    pub archived: u8,
193    /// Additional add-on information associated with this product, if any.
194    #[serde(rename = "addOnInfo")]
195    pub add_on_info: Option<String>,
196    /// The unique identifier for this order-product record.
197    #[serde(rename = "orderProductId")]
198    pub order_product_id: u64,
199    /// The unique identifier of the customer who owns this order.
200    #[serde(rename = "customerId")]
201    pub customer_id: u64,
202    /// The date the product files were last modified (ISO 8601 string), if known.
203    #[serde(rename = "fileLastModified")]
204    pub file_last_modified: Option<String>,
205    /// The date the product files were last downloaded (ISO 8601 string), if known.
206    #[serde(rename = "fileLastDownloaded")]
207    pub file_last_downloaded: Option<String>,
208    /// The list of downloadable files associated with this ordered product.
209    pub files: Vec<OrderProductFile>,
210    /// Filter categories for this product. Populated when `getFilters=1` is requested.
211    pub filters: Option<Vec<OrderProductFilter>>,
212    /// The change history for this ordered product, if requested.
213    pub history: Option<Vec<OrderProductHistoryEntry>>,
214    /// Optional attributes describing purchase options (format, edition, etc.).
215    pub attributes: Option<Vec<OrderProductAttribute>>,
216    /// Publisher metadata embedded directly on this ordered product's attributes, when the
217    /// API includes it inline (in addition to, or instead of, sideloaded `included` publisher
218    /// resources).
219    #[serde(default)]
220    pub publisher: Option<OrderProductPublisher>,
221    /// Product catalog metadata (cover images, description) embedded on this ordered product.
222    #[serde(default)]
223    pub product: Option<OrderProductInfo>,
224    /// Order summary metadata embedded on this ordered product.
225    #[serde(default)]
226    pub order: Option<OrderProductOrder>,
227}
228
229/// Publisher metadata embedded directly on an ordered product's attributes.
230#[derive(Clone, Debug, Deserialize, Serialize)]
231pub struct OrderProductPublisher {
232    /// The display name of the publisher.
233    pub name: String,
234    /// The unique identifier of the publisher.
235    #[serde(rename = "publisherId")]
236    pub publisher_id: u64,
237    /// The URL slug for the publisher's storefront page.
238    pub slug: String,
239}
240
241/// Descriptive text for a product, embedded within [`OrderProductInfo`].
242#[derive(Clone, Debug, Deserialize, Serialize)]
243pub struct OrderProductDescription {
244    /// The display name of the product.
245    pub name: String,
246    /// HTML purchase note shown to the customer, if any.
247    #[serde(rename = "purchaseNote", default)]
248    pub purchase_note: Option<String>,
249    /// The URL slug for the product's storefront page.
250    pub slug: String,
251    /// A short marketing description of the product.
252    #[serde(rename = "shortDescription", default)]
253    pub short_description: Option<String>,
254}
255
256/// Product catalog metadata embedded directly on an ordered product's attributes, including
257/// relative paths to cover images.
258///
259/// Image paths (`image`, `web_image`, `thumbnail`, `thumbnail_100`) are relative to the
260/// DriveThruRPG images base URL (`https://api.drivethrurpg.com/images/`).
261#[derive(Clone, Debug, Deserialize, Serialize)]
262pub struct OrderProductInfo {
263    /// Relative path to the full-size cover image, if available.
264    #[serde(default)]
265    pub image: Option<String>,
266    /// Relative path to the web-optimized (WebP) cover image, if available.
267    #[serde(rename = "webImage", default)]
268    pub web_image: Option<String>,
269    /// Relative path to the 140px cover thumbnail image, if available.
270    #[serde(default)]
271    pub thumbnail: Option<String>,
272    /// Relative path to the 100px cover thumbnail image, if available.
273    #[serde(rename = "thumbnail100", default)]
274    pub thumbnail_100: Option<String>,
275    /// Bundle ID if this product is part of a bundle, otherwise 0.
276    #[serde(rename = "bundleId")]
277    pub bundle_id: u64,
278    /// Date and time when the product was added to the DTRPG catalog, if known.
279    #[serde(rename = "dateCreated", default)]
280    pub date_created: Option<String>,
281    /// Unique identifier for the product in the DTRPG catalog.
282    #[serde(rename = "productId")]
283    pub product_id: u64,
284    /// Descriptive text for the product, if requested.
285    #[serde(default)]
286    pub description: Option<OrderProductDescription>,
287    /// Total file size in megabytes, if known.
288    #[serde(default)]
289    pub filesize: Option<f64>,
290}
291
292/// Order summary metadata embedded on an ordered product's attributes.
293#[derive(Clone, Debug, Deserialize, Serialize)]
294pub struct OrderProductOrder {
295    /// Date and time when the order was created, if known.
296    #[serde(rename = "dateCreated", default)]
297    pub date_created: Option<String>,
298    /// The unique identifier of the order.
299    #[serde(rename = "orderId")]
300    pub order_id: u64,
301}
302
303/// A single item in an ordered products collection response.
304///
305/// Follows the JSON:API resource object structure with `id`, `type`, and `attributes`.
306///
307/// The live API does *not* embed `publisher`/`product`/`order` metadata directly on
308/// `attributes` for this endpoint (despite what earlier documentation examples showed) —
309/// it references them via `relationships`, resolved against the response's top-level
310/// `included` array. See [`OrderProductRelationships`] and [`IncludedItem`].
311#[derive(Clone, Debug, Deserialize, Serialize)]
312pub struct OrderProductItem {
313    /// The JSON:API resource identifier.
314    pub id: String,
315    /// The JSON:API resource type string (e.g., `"order_product"`).
316    #[serde(rename = "type")]
317    pub resource_type: String,
318    /// The full attribute set for this ordered product.
319    pub attributes: OrderProductAttributes,
320    /// JSON:API relationship references to sideloaded `Publisher`/`Product`/`Order`
321    /// resources, resolved by matching `id` against the response's `included` array.
322    #[serde(default)]
323    pub relationships: Option<OrderProductRelationships>,
324}
325
326/// JSON:API relationship references carried on an [`OrderProductItem`].
327#[derive(Clone, Debug, Deserialize, Serialize)]
328pub struct OrderProductRelationships {
329    /// Reference to the sideloaded `Publisher` resource, if present.
330    #[serde(default)]
331    pub publisher: Option<RelationshipRef>,
332    /// Reference to the sideloaded `Product` resource, if present.
333    #[serde(default)]
334    pub product: Option<RelationshipRef>,
335    /// Reference to the sideloaded `Order` resource, if present.
336    #[serde(default)]
337    pub order: Option<RelationshipRef>,
338}
339
340/// A single JSON:API relationship reference, wrapping the `data` resource identifier.
341#[derive(Clone, Debug, Deserialize, Serialize)]
342pub struct RelationshipRef {
343    /// The referenced resource's type and id, if the relationship is populated.
344    pub data: Option<RelationshipData>,
345}
346
347/// The `type`/`id` pair identifying a JSON:API resource referenced by a relationship.
348#[derive(Clone, Debug, Deserialize, Serialize)]
349pub struct RelationshipData {
350    /// The referenced resource's type string (e.g., `"Product"`).
351    #[serde(rename = "type")]
352    pub resource_type: String,
353    /// The referenced resource's id, matching an entry's `id` in the `included` array.
354    pub id: String,
355}
356
357// ── Sideloaded resources (`included`) ────────────────────────────────────────
358
359/// Attributes for a publisher resource included alongside ordered product responses.
360#[derive(Clone, Debug, Deserialize, Serialize)]
361pub struct PublisherAttributes {
362    /// The display name of the publisher.
363    #[serde(default)]
364    pub name: String,
365    /// The unique identifier of the publisher.
366    #[serde(rename = "publisherId", default)]
367    pub publisher_id: u64,
368    /// The URL slug for the publisher's storefront page.
369    #[serde(default)]
370    pub slug: String,
371}
372
373/// A publisher resource item included in ordered product responses when requested.
374///
375/// Follows the JSON:API resource object structure.
376#[derive(Clone, Debug, Deserialize, Serialize)]
377pub struct PublisherItem {
378    /// The JSON:API resource identifier.
379    pub id: String,
380    /// The JSON:API resource type string (e.g., `"publisher"`).
381    #[serde(rename = "type")]
382    pub resource_type: String,
383    /// The publisher attributes.
384    pub attributes: PublisherAttributes,
385}
386
387/// A single sideloaded resource entity from the `included` array of an ordered-products
388/// list response.
389///
390/// The `included` array mixes multiple JSON:API resource types (`Publisher`, `Product`,
391/// `Order`) in a single flat list; `resource_type` disambiguates which, and `attributes`
392/// is kept as an untyped [`serde_json::Value`] since its shape depends on `resource_type`.
393/// Decode it via [`IncludedItem::as_publisher`] or [`IncludedItem::as_product`].
394#[derive(Clone, Debug, Deserialize, Serialize)]
395pub struct IncludedItem {
396    /// The JSON:API resource identifier. Matches a [`RelationshipData::id`] referencing it.
397    pub id: String,
398    /// The JSON:API resource type string (e.g., `"Publisher"`, `"Product"`, `"Order"`).
399    #[serde(rename = "type")]
400    pub resource_type: String,
401    /// The resource's untyped attribute payload; shape depends on `resource_type`.
402    pub attributes: serde_json::Value,
403}
404
405impl IncludedItem {
406    /// Decodes `attributes` as [`PublisherAttributes`] if `resource_type == "Publisher"`.
407    ///
408    /// Returns `None` for any other resource type or if decoding fails.
409    #[must_use]
410    pub fn as_publisher(&self) -> Option<PublisherAttributes> {
411        if self.resource_type != "Publisher" {
412            return None;
413        }
414        serde_json::from_value(self.attributes.clone()).ok()
415    }
416
417    /// Decodes `attributes` as [`OrderProductInfo`] if `resource_type == "Product"`.
418    ///
419    /// Returns `None` for any other resource type or if decoding fails.
420    #[must_use]
421    pub fn as_product(&self) -> Option<OrderProductInfo> {
422        if self.resource_type != "Product" {
423            return None;
424        }
425        serde_json::from_value(self.attributes.clone()).ok()
426    }
427}
428
429// ── Response wrappers ─────────────────────────────────────────────────────────
430
431/// A paginated collection of ordered products.
432///
433/// Returned by `GET /{api_version}/order_products`.
434#[derive(Clone, Debug, Deserialize, Serialize)]
435pub struct OrderProductListResponse {
436    /// Pagination links for navigating the result set.
437    pub links: PaginationLinks,
438    /// Pagination metadata describing the current page.
439    pub meta: PaginationMeta,
440    /// The ordered product items on this page.
441    pub data: Vec<OrderProductItem>,
442    /// Publisher/Product/Order resources sideloaded alongside the ordered products.
443    pub included: Option<Vec<IncludedItem>>,
444}
445
446/// A single ordered product resource response.
447///
448/// Returned by `GET /{api_version}/order_products/{id}`.
449#[derive(Clone, Debug, Deserialize, Serialize)]
450pub struct OrderProductItemResponse {
451    /// The ordered product item.
452    pub data: OrderProductItem,
453    /// Publisher/Product/Order resources sideloaded alongside the ordered product,
454    /// resolved by matching `relationships.*.data.id` against each entry's `id`
455    /// (mirrors [`OrderProductListResponse::included`]).
456    #[serde(default)]
457    pub included: Option<Vec<IncludedItem>>,
458}
459
460// ── Product Lists ─────────────────────────────────────────────────────────────
461
462/// Attributes for a product list resource.
463#[derive(Clone, Debug, Deserialize, Serialize)]
464pub struct ProductListAttributes {
465    /// The identifier of the customer who owns this list.
466    #[serde(rename = "customerId")]
467    pub customer_id: u64,
468    /// The display name of the product list.
469    pub name: String,
470    /// The date the list was created (ISO 8601 string).
471    #[serde(rename = "dateCreated")]
472    pub date_created: String,
473    /// The unique identifier for this product list.
474    #[serde(rename = "productListId")]
475    pub product_list_id: u64,
476    /// The URL slug for this product list.
477    pub slug: String,
478    /// The number of items currently in this product list.
479    #[serde(rename = "itemCount")]
480    pub item_count: u64,
481}
482
483/// A single product list resource item.
484///
485/// Follows the JSON:API resource object structure.
486#[derive(Clone, Debug, Deserialize, Serialize)]
487pub struct ProductListItem {
488    /// The JSON:API resource identifier.
489    pub id: String,
490    /// The JSON:API resource type string (e.g., `"product_list"`).
491    #[serde(rename = "type")]
492    pub resource_type: String,
493    /// The product list attributes.
494    pub attributes: ProductListAttributes,
495}
496
497/// A paginated collection of product lists belonging to the authenticated customer.
498///
499/// Returned by `GET /{api_version}/product_lists`.
500#[derive(Clone, Debug, Deserialize, Serialize)]
501pub struct ProductListCollectionResponse {
502    /// Pagination links for navigating the result set.
503    pub links: PaginationLinks,
504    /// Pagination metadata describing the current page.
505    pub meta: PaginationMeta,
506    /// The product list items on this page.
507    pub data: Vec<ProductListItem>,
508}
509
510/// A paginated collection of items within a specific product list.
511///
512/// Returned by `GET /{api_version}/product_list_items`. Individual item schemas are
513/// not yet formally defined by the API contract, so items are represented as raw
514/// [`serde_json::Value`]s until the schema matures.
515#[derive(Clone, Debug, Deserialize, Serialize)]
516pub struct ProductListItemsResponse {
517    /// Pagination links for navigating the result set.
518    pub links: PaginationLinks,
519    /// Pagination metadata describing the current page.
520    pub meta: PaginationMeta,
521    /// The raw product list item data on this page.
522    pub data: Vec<serde_json::Value>,
523}
524
525/// Request body for adding a product to a product list.
526///
527/// Sent by `POST /{api_version}/product_list_items`.
528#[derive(Clone, Debug, Serialize)]
529pub struct ProductListItemCreateRequest {
530    /// Unique identifier of the product to add.
531    #[serde(rename = "productId")]
532    pub product_id: u64,
533    /// Unique identifier of the product list to add the product to.
534    #[serde(rename = "productListId")]
535    pub product_list_id: u64,
536}
537
538/// The created product list item.
539///
540/// Returned by `POST /{api_version}/product_list_items`. The API wraps this resource
541/// in a JSON:API-style envelope on the wire (`{"data": {"id": ..., "type": ...,
542/// "attributes": {"productId": ..., "productListId": ..., "productListItemId": ...}}}`);
543/// [`Deserialize`] unwraps that envelope so callers work with a flat struct.
544#[derive(Clone, Debug, Serialize)]
545pub struct ProductListItemCreateResponse {
546    /// Unique identifier of the product added to the list.
547    pub product_id: u64,
548    /// Unique identifier of the product list the product was added to.
549    pub product_list_id: u64,
550    /// Unique identifier assigned to this product list item. Required to remove
551    /// the item later via `DELETE /{api_version}/product_list_items/{id}`.
552    pub product_list_item_id: u64,
553}
554
555impl<'de> Deserialize<'de> for ProductListItemCreateResponse {
556    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
557    where
558        D: Deserializer<'de>,
559    {
560        #[derive(Deserialize)]
561        struct Attributes {
562            #[serde(rename = "productId")]
563            product_id: u64,
564            #[serde(rename = "productListId")]
565            product_list_id: u64,
566            #[serde(rename = "productListItemId")]
567            product_list_item_id: u64,
568        }
569        #[derive(Deserialize)]
570        struct Resource {
571            attributes: Attributes,
572        }
573        #[derive(Deserialize)]
574        struct Envelope {
575            data: Resource,
576        }
577
578        let envelope = Envelope::deserialize(deserializer)?;
579        Ok(Self {
580            product_id: envelope.data.attributes.product_id,
581            product_list_id: envelope.data.attributes.product_list_id,
582            product_list_item_id: envelope.data.attributes.product_list_item_id,
583        })
584    }
585}
586
587// ── Query parameter structs ───────────────────────────────────────────────────
588
589/// Query parameters for the `GET /order_products` (library items) endpoint.
590///
591/// All fields are optional. Set a field to `Some(value)` to include the corresponding
592/// query parameter in the request. Use [`Default::default()`] to start with no filters
593/// applied.
594///
595/// # Examples
596///
597/// ```rust
598/// use dtrpg_sdk::LibraryItemsParams;
599///
600/// let params = LibraryItemsParams {
601///     page: Some(2),
602///     page_size: Some(50),
603///     get_filters: Some(true),
604///     ..Default::default()
605/// };
606/// ```
607#[derive(Default)]
608pub struct LibraryItemsParams {
609    /// The page number to retrieve (1-based).
610    pub page: Option<u32>,
611    /// The number of items to return per page.
612    pub page_size: Option<u32>,
613    /// When `true`, includes checksum data for each product file (`getChecksum=1`).
614    pub get_checksum: Option<bool>,
615    /// When `true`, includes filter category data for each product (`getFilters=1`).
616    pub get_filters: Option<bool>,
617    /// When `true`, restricts results to library (non-archived) products (`library=true`).
618    pub library: Option<bool>,
619    /// When `true`, includes archived products; when `false`, excludes them (`archived=1/0`).
620    pub archived: Option<bool>,
621    /// ISO 8601 date string. When set, returns only products updated after this date
622    /// (`updatedDate[after]=...`).
623    pub updated_date_after: Option<String>,
624}
625
626/// Query parameters for paginated collection endpoints such as `/product_lists`.
627///
628/// All fields are optional. Use [`Default::default()`] to retrieve the first page with
629/// the server's default page size.
630///
631/// # Examples
632///
633/// ```rust
634/// use dtrpg_sdk::PageParams;
635///
636/// let params = PageParams { page: Some(3), page_size: Some(25) };
637/// ```
638#[derive(Default)]
639pub struct PageParams {
640    /// The page number to retrieve (1-based).
641    pub page: Option<u32>,
642    /// The number of items to return per page.
643    pub page_size: Option<u32>,
644}