Skip to main content

shopify_sdk/
lib.rs

1//! # Shopify API Rust SDK
2//!
3//! A Rust SDK for the Shopify API, providing type-safe configuration,
4//! authentication handling, and HTTP client functionality for Shopify app development.
5//!
6//! ## Overview
7//!
8//! This SDK provides:
9//! - Type-safe configuration via [`ShopifyConfig`] and [`ShopifyConfigBuilder`]
10//! - Validated newtypes for API credentials and domain values
11//! - OAuth scope handling with implied scope support
12//! - OAuth 2.0 authorization code flow via [`auth::oauth`]
13//! - Token exchange for embedded apps via [`auth::oauth`]
14//! - Client credentials for private/organization apps via [`auth::oauth`]
15//! - Token refresh for expiring access tokens via [`auth::oauth`]
16//! - Session management for authenticated API calls
17//! - Async HTTP client with retry logic and rate limit handling
18//! - REST API client with convenient methods for Admin API operations
19//! - REST resource infrastructure with CRUD operations and dirty tracking
20//! - GraphQL API client for modern Admin API operations (recommended)
21//! - Storefront API client for headless commerce applications
22//! - Webhook registration system for event subscriptions
23//!
24//! ## Quick Start
25//!
26//! ```rust
27//! use shopify_sdk::{ShopifyConfig, ApiKey, ApiSecretKey, ApiVersion, AuthScopes};
28//!
29//! // Create configuration using the builder pattern
30//! let config = ShopifyConfig::builder()
31//!     .api_key(ApiKey::new("your-api-key").unwrap())
32//!     .api_secret_key(ApiSecretKey::new("your-api-secret").unwrap())
33//!     .scopes("read_products,write_orders".parse().unwrap())
34//!     .api_version(ApiVersion::latest())
35//!     .build()
36//!     .unwrap();
37//! ```
38//!
39//! ## OAuth Authentication
40//!
41//! For apps that need to authenticate with Shopify stores:
42//!
43//! ```rust,ignore
44//! use shopify_sdk::{ShopifyConfig, ApiKey, ApiSecretKey, ShopDomain, HostUrl};
45//! use shopify_sdk::auth::oauth::{begin_auth, validate_auth_callback, AuthQuery};
46//!
47//! // Step 1: Configure the SDK
48//! let config = ShopifyConfig::builder()
49//!     .api_key(ApiKey::new("your-api-key").unwrap())
50//!     .api_secret_key(ApiSecretKey::new("your-secret").unwrap())
51//!     .host(HostUrl::new("https://your-app.com").unwrap())
52//!     .scopes("read_products".parse().unwrap())
53//!     .build()
54//!     .unwrap();
55//!
56//! // Step 2: Begin authorization
57//! let shop = ShopDomain::new("example-shop").unwrap();
58//! let result = begin_auth(&config, &shop, "/auth/callback", true, None)?;
59//! // Redirect user to result.auth_url
60//! // Store result.state in session
61//!
62//! // Step 3: Handle callback
63//! let session = validate_auth_callback(&config, &query, &stored_state).await?;
64//! // session is now ready for API calls
65//! ```
66//!
67//! ## Token Exchange (Embedded Apps)
68//!
69//! For embedded apps using App Bridge session tokens:
70//!
71//! ```rust,ignore
72//! use shopify_sdk::{ShopifyConfig, ApiKey, ApiSecretKey, ShopDomain};
73//! use shopify_sdk::auth::oauth::{exchange_online_token, exchange_offline_token};
74//!
75//! // Configure the SDK (must be embedded)
76//! let config = ShopifyConfig::builder()
77//!     .api_key(ApiKey::new("your-api-key").unwrap())
78//!     .api_secret_key(ApiSecretKey::new("your-secret").unwrap())
79//!     .is_embedded(true)
80//!     .build()
81//!     .unwrap();
82//!
83//! let shop = ShopDomain::new("example-shop").unwrap();
84//! let session_token = "eyJ..."; // JWT from App Bridge
85//!
86//! // Exchange for an online access token
87//! let session = exchange_online_token(&config, &shop, session_token).await?;
88//!
89//! // Or exchange for an offline access token
90//! let session = exchange_offline_token(&config, &shop, session_token).await?;
91//! ```
92//!
93//! ## Client Credentials (Private/Organization Apps)
94//!
95//! For private or organization apps without user interaction:
96//!
97//! ```rust,ignore
98//! use shopify_sdk::{ShopifyConfig, ApiKey, ApiSecretKey, ShopDomain};
99//! use shopify_sdk::auth::oauth::exchange_client_credentials;
100//!
101//! // Configure the SDK (must NOT be embedded)
102//! let config = ShopifyConfig::builder()
103//!     .api_key(ApiKey::new("your-api-key").unwrap())
104//!     .api_secret_key(ApiSecretKey::new("your-secret").unwrap())
105//!     // is_embedded defaults to false, which is required
106//!     .build()
107//!     .unwrap();
108//!
109//! let shop = ShopDomain::new("example-shop").unwrap();
110//!
111//! // Exchange client credentials for an offline access token
112//! let session = exchange_client_credentials(&config, &shop).await?;
113//! println!("Access token: {}", session.access_token);
114//! ```
115//!
116//! ## Token Refresh (Expiring Tokens)
117//!
118//! For apps using expiring offline access tokens:
119//!
120//! ```rust,ignore
121//! use shopify_sdk::{ShopifyConfig, ApiKey, ApiSecretKey, ShopDomain};
122//! use shopify_sdk::auth::oauth::{refresh_access_token, migrate_to_expiring_token};
123//!
124//! // Refresh an expiring access token
125//! if session.expired() {
126//!     if let Some(refresh_token) = &session.refresh_token {
127//!         let new_session = refresh_access_token(&config, &shop, refresh_token).await?;
128//!         println!("New access token: {}", new_session.access_token);
129//!     }
130//! }
131//!
132//! // Or migrate from non-expiring to expiring tokens (one-time, irreversible)
133//! let new_session = migrate_to_expiring_token(&config, &shop, &old_access_token).await?;
134//! ```
135//!
136//! ## Session Management
137//!
138//! Sessions represent authenticated connections to a Shopify store. They can be
139//! either offline (app-level) or online (user-specific):
140//!
141//! ```rust
142//! use shopify_sdk::{Session, ShopDomain, AuthScopes, AssociatedUser};
143//!
144//! // Create an offline session (no expiration, no user)
145//! let offline_session = Session::new(
146//!     Session::generate_offline_id(&ShopDomain::new("my-store").unwrap()),
147//!     ShopDomain::new("my-store").unwrap(),
148//!     "access-token".to_string(),
149//!     "read_products".parse().unwrap(),
150//!     false,
151//!     None,
152//! );
153//!
154//! // Sessions can be serialized for storage
155//! let json = serde_json::to_string(&offline_session).unwrap();
156//! ```
157//!
158//! ## Making GraphQL API Requests (Recommended)
159//!
160//! The [`GraphqlClient`] provides methods for GraphQL Admin API operations:
161//!
162//! ```rust,ignore
163//! use shopify_sdk::{GraphqlClient, Session, ShopDomain, AuthScopes};
164//! use serde_json::json;
165//!
166//! // Create a session
167//! let session = Session::new(
168//!     "session-id".to_string(),
169//!     ShopDomain::new("my-store").unwrap(),
170//!     "access-token".to_string(),
171//!     AuthScopes::new(),
172//!     false,
173//!     None,
174//! );
175//!
176//! // Create a GraphQL client (no deprecation warning - this is the recommended API)
177//! let client = GraphqlClient::new(&session, None);
178//!
179//! // Simple query
180//! let response = client.query("query { shop { name } }", None, None, None).await?;
181//! println!("Shop: {}", response.body["data"]["shop"]["name"]);
182//!
183//! // Query with variables
184//! let response = client.query(
185//!     "query GetProduct($id: ID!) { product(id: $id) { title } }",
186//!     Some(json!({ "id": "gid://shopify/Product/123" })),
187//!     None,
188//!     None
189//! ).await?;
190//!
191//! // Check for GraphQL errors (returned with HTTP 200)
192//! if let Some(errors) = response.body.get("errors") {
193//!     println!("GraphQL errors: {}", errors);
194//! }
195//! ```
196//!
197//! ## Making Storefront API Requests
198//!
199//! The [`StorefrontClient`] provides methods for Storefront API operations:
200//!
201//! ```rust,ignore
202//! use shopify_sdk::{StorefrontClient, StorefrontToken, ShopDomain};
203//! use serde_json::json;
204//!
205//! let shop = ShopDomain::new("my-store").unwrap();
206//!
207//! // With public token (client-side safe)
208//! let token = StorefrontToken::Public("public-access-token".to_string());
209//! let client = StorefrontClient::new(&shop, Some(token), None);
210//!
211//! // With private token (server-side only)
212//! let token = StorefrontToken::Private("private-access-token".to_string());
213//! let client = StorefrontClient::new(&shop, Some(token), None);
214//!
215//! // Tokenless access for basic features
216//! let client = StorefrontClient::new(&shop, None, None);
217//!
218//! // Query products
219//! let response = client.query(
220//!     "query { products(first: 10) { edges { node { title } } } }",
221//!     None,
222//!     None,
223//!     None
224//! ).await?;
225//!
226//! // Access response data
227//! let products = &response.body["data"]["products"];
228//! ```
229//!
230//! ## Webhook Registration
231//!
232//! The [`WebhookRegistry`] provides webhook subscription management with support
233//! for HTTP, Amazon EventBridge, and Google Cloud Pub/Sub delivery methods:
234//!
235//! ```rust
236//! use shopify_sdk::{
237//!     WebhookRegistry, WebhookRegistrationBuilder, WebhookTopic, WebhookDeliveryMethod
238//! };
239//!
240//! // Configure webhooks at startup
241//! let mut registry = WebhookRegistry::new();
242//!
243//! // HTTP delivery
244//! registry
245//!     .add_registration(
246//!         WebhookRegistrationBuilder::new(
247//!             WebhookTopic::OrdersCreate,
248//!             WebhookDeliveryMethod::Http {
249//!                 uri: "https://example.com/api/webhooks/orders".to_string(),
250//!             },
251//!         )
252//!         .build()
253//!     )
254//!     // Amazon EventBridge delivery
255//!     .add_registration(
256//!         WebhookRegistrationBuilder::new(
257//!             WebhookTopic::ProductsUpdate,
258//!             WebhookDeliveryMethod::EventBridge {
259//!                 arn: "arn:aws:events:us-east-1::event-source/aws.partner/shopify.com/123/source".to_string(),
260//!             },
261//!         )
262//!         .build()
263//!     )
264//!     // Google Cloud Pub/Sub delivery
265//!     .add_registration(
266//!         WebhookRegistrationBuilder::new(
267//!             WebhookTopic::CustomersCreate,
268//!             WebhookDeliveryMethod::PubSub {
269//!                 project_id: "my-gcp-project".to_string(),
270//!                 topic_id: "shopify-webhooks".to_string(),
271//!             },
272//!         )
273//!         .filter("vendor:MyApp".to_string())
274//!         .build()
275//!     );
276//!
277//! // Later, register with Shopify when session is available:
278//! // let results = registry.register_all(&session, &config).await?;
279//! ```
280//!
281//! ## Making REST API Requests (Deprecated)
282//!
283//! The [`RestClient`] provides convenient methods for REST API operations:
284//!
285//! ```rust,ignore
286//! use shopify_sdk::{RestClient, Session, ShopDomain, AuthScopes};
287//!
288//! // Create a session
289//! let session = Session::new(
290//!     "session-id".to_string(),
291//!     ShopDomain::new("my-store").unwrap(),
292//!     "access-token".to_string(),
293//!     AuthScopes::new(),
294//!     false,
295//!     None,
296//! );
297//!
298//! // Create a REST client (logs deprecation warning)
299//! let client = RestClient::new(&session, None)?;
300//!
301//! // GET request
302//! let response = client.get("products", None).await?;
303//!
304//! // POST request with body
305//! let body = serde_json::json!({"product": {"title": "New Product"}});
306//! let response = client.post("products", body, None).await?;
307//! ```
308//!
309//! ## Making Low-Level HTTP Requests
310//!
311//! For more control, use the low-level [`HttpClient`]:
312//!
313//! ```rust,ignore
314//! use shopify_sdk::{Session, ShopDomain, AuthScopes};
315//! use shopify_sdk::clients::{HttpClient, HttpRequest, HttpMethod};
316//!
317//! // Create a session
318//! let session = Session::new(
319//!     "session-id".to_string(),
320//!     ShopDomain::new("my-store").unwrap(),
321//!     "access-token".to_string(),
322//!     AuthScopes::new(),
323//!     false,
324//!     None,
325//! );
326//!
327//! // Create an HTTP client
328//! let client = HttpClient::new("/admin/api/2024-10", &session, None);
329//!
330//! // Build and send a request
331//! let request = HttpRequest::builder(HttpMethod::Get, "products.json")
332//!     .build()
333//!     .unwrap();
334//!
335//! let response = client.request(request).await?;
336//! ```
337//!
338//! ## Design Principles
339//!
340//! - **No global state**: Configuration is instance-based and passed explicitly
341//! - **Fail-fast validation**: All newtypes validate on construction
342//! - **Thread-safe**: All types are `Send + Sync`
343//! - **Async-first**: Designed for use with Tokio async runtime
344//! - **Immutable sessions**: Sessions are immutable after creation
345
346pub mod auth;
347pub mod clients;
348pub mod config;
349pub mod error;
350pub mod rest;
351pub mod webhooks;
352
353// Re-export public types at crate root for convenience
354pub use auth::{AssociatedUser, AuthScopes, Session};
355pub use config::{
356    ApiKey, ApiSecretKey, ApiVersion, DeprecationCallback, HostUrl, ShopDomain, ShopifyConfig,
357    ShopifyConfigBuilder,
358};
359pub use error::ConfigError;
360
361// Re-export HTTP client types
362pub use clients::{
363    ApiCallLimit, ApiDeprecationInfo, DataType, HttpClient, HttpError, HttpMethod, HttpRequest,
364    HttpRequestBuilder, HttpResponse, HttpResponseError, InvalidHttpRequestError,
365    MaxHttpRetriesExceededError, PaginationInfo,
366};
367
368// Re-export REST client types
369pub use clients::{RestClient, RestError};
370
371// Re-export GraphQL client types
372pub use clients::{GraphqlClient, GraphqlError};
373
374// Re-export Storefront client types
375pub use clients::{StorefrontClient, StorefrontToken};
376
377// Re-export OAuth types for convenience
378pub use auth::oauth::{
379    begin_auth, exchange_client_credentials, exchange_offline_token, exchange_online_token,
380    migrate_to_expiring_token, refresh_access_token, validate_auth_callback, AuthQuery,
381    BeginAuthResult, OAuthError, StateParam,
382};
383
384// Re-export REST resource types for convenience
385pub use rest::{
386    ResourceError, ResourceOperation, ResourcePath, ResourceResponse, RestResource, TrackedResource,
387};
388
389// Re-export webhook types for convenience
390pub use webhooks::{
391    WebhookDeliveryMethod, WebhookError, WebhookRegistration, WebhookRegistrationBuilder,
392    WebhookRegistrationResult, WebhookRegistry, WebhookTopic,
393};