Skip to main content

dtrpg_sdk/
lib.rs

1//! # DriveThruRPG SDK
2//!
3//! A Rust SDK for interacting with the [DriveThruRPG API](https://api.drivethrurpg.com).
4//!
5//! ## Overview
6//!
7//! This crate provides types and structures for authenticating with, configuring, and
8//! making requests to the DriveThruRPG API. It covers:
9//!
10//! - **Configuration** — supplying your application key, API base URL, and API version
11//!   via [`Config`].
12//! - **Authentication** — representing token responses, active sessions, and session state
13//!   via [`AuthTokenResponse`], [`AuthSession`], and [`AuthState`].
14//! - **Error handling** — structured errors for SDK-level, session-level, and HTTP failures
15//!   via [`SdkError`], [`AuthSessionError`], and [`ClientError`].
16//! - **SDK entry point** — [`DriveThruRpgSdk`] ties configuration and session lifecycle
17//!   together and vends a [`LibraryClient`] once authenticated.
18//! - **Library access** — [`LibraryClient`] provides an async HTTP client for all library
19//!   endpoints (ordered products, product lists, download preparation).
20//! - **Library types** — Rust model types for every API-defined library schema, such as
21//!   [`OrderProductItem`], [`ProductListItem`], and their supporting structures.
22//!
23//! ## Quick Start
24//!
25//! ```rust
26//! use dtrpg_sdk::{Config, DriveThruRpgSdk, AuthTokenResponse};
27//!
28//! let mut sdk = DriveThruRpgSdk::with_config(Config::new("my-app-key"));
29//!
30//! // After receiving an auth response from the API:
31//! let response = AuthTokenResponse::new("jwt-token", "refresh-token", 1_800_000_000);
32//! let session = sdk.apply_auth_response(response).unwrap();
33//! assert_eq!(session.token(), "jwt-token");
34//!
35//! // Create an authenticated library client:
36//! let client = sdk.library_client().unwrap();
37//! // client.list_order_products(Default::default()).await ...
38//! ```
39
40pub mod auth;
41pub mod config;
42pub mod error;
43pub mod library;
44pub mod openapi;
45pub mod sdk;
46
47pub use auth::{AuthSession, AuthState, AuthTokenResponse, SessionTransition};
48pub use config::Config;
49pub use error::{AuthSessionError, SdkError};
50pub use library::{
51    ClientError, FileChecksum, IncludedItem, LibraryClient, LibraryItemsParams,
52    OrderProductAttribute, OrderProductAttributes, OrderProductDescription, OrderProductFile,
53    OrderProductFilter, OrderProductHistoryEntry, OrderProductInfo, OrderProductItem,
54    OrderProductItemResponse, OrderProductListResponse, OrderProductOrder, OrderProductPublisher,
55    OrderProductRelationships, PageParams, PaginationLinks, PaginationMeta, ProductListAttributes,
56    ProductListCollectionResponse, ProductListItem, ProductListItemCreateRequest,
57    ProductListItemCreateResponse, ProductListItemsResponse, PublisherAttributes, PublisherItem,
58    RelationshipData, RelationshipRef,
59};
60pub use openapi::{OPERATIONS, OpenApiOperation};
61pub use sdk::DriveThruRpgSdk;
62
63#[cfg(test)]
64mod tests {
65    use super::{
66        AuthSessionError, AuthState, AuthTokenResponse, Config, DriveThruRpgSdk, SdkError,
67    };
68
69    #[test]
70    fn sdk_requires_configuration_before_auth_session_is_applied() {
71        let mut sdk = DriveThruRpgSdk::new();
72        let response = AuthTokenResponse::new("jwt-token", "refresh-token", 1_771_547_233);
73
74        let error = sdk.apply_auth_response(response).unwrap_err();
75
76        assert_eq!(error, SdkError::Unconfigured);
77    }
78
79    #[test]
80    fn sdk_stores_api_defined_auth_session_after_configuration() {
81        let mut sdk = DriveThruRpgSdk::with_config(Config::new("app-key"));
82        let response = AuthTokenResponse::new("jwt-token", "refresh-token", 1_771_547_233);
83
84        let session = sdk.apply_auth_response(response).unwrap();
85
86        assert_eq!(session.token(), "jwt-token");
87        assert_eq!(session.refresh_token(), "refresh-token");
88        assert!(!session.refresh_token_expired_at(1_771_547_232));
89        assert!(session.refresh_token_expired_at(1_771_547_233));
90    }
91
92    #[test]
93    fn invalidating_session_preserves_api_auth_state_meaning() {
94        let mut sdk = DriveThruRpgSdk::with_config(Config::new("app-key"));
95        let response = AuthTokenResponse::new("jwt-token", "refresh-token", 1_771_547_233);
96        sdk.apply_auth_response(response).unwrap();
97
98        let error = AuthSessionError::new(
99            "token_expired",
100            "The authentication token has expired.",
101            AuthState::TokenExpired,
102        );
103
104        let invalidation = sdk.invalidate_session(error.clone()).unwrap();
105
106        assert_eq!(invalidation, error);
107        assert_eq!(
108            sdk.require_session().unwrap_err(),
109            SdkError::Unauthenticated
110        );
111    }
112
113    #[test]
114    fn rust_sdk_generates_api_metadata_from_openapi_spec() {
115        assert_eq!(
116            crate::openapi::DEFAULT_SERVER_URL,
117            "https://api.drivethrurpg.com/api"
118        );
119        const { assert!(crate::openapi::OPENAPI_SPEC_BYTES > 0) };
120        assert!(
121            crate::openapi::OPERATIONS.contains(&crate::OpenApiOperation {
122                method: "POST",
123                path: "/{DTRPG_API_VERSION}/auth_key",
124            })
125        );
126        assert!(
127            crate::openapi::OPERATIONS.contains(&crate::OpenApiOperation {
128                method: "GET",
129                path: "/{DTRPG_API_VERSION}/order_products",
130            })
131        );
132    }
133}