# FlowGlad Rust SDK - Implementation Plan
> **Goal**: Create a Rust SDK that is clear, simple, and impressive to expert Rust programmers through idiomatic patterns, type safety, and elegant API design.
## Table of Contents
1. [Core Design Principles](#core-design-principles)
2. [Architecture Overview](#architecture-overview)
3. [Implementation Phases](#implementation-phases)
4. [Code Quality Guidelines](#code-quality-guidelines)
5. [Testing Strategy](#testing-strategy)
6. [Future Considerations](#future-considerations)
---
## Core Design Principles
### What Makes This SDK "Expert-Level Impressive"
**1. Type-Driven Design**
- Use Rust's type system to make invalid states unrepresentable
- Leverage newtypes for domain-specific identifiers (e.g., `CustomerId`, `ProductId`)
- Use enums with associated data for API responses with variants
- Builder pattern with compile-time validation using typestate pattern
**2. Zero-Cost Abstractions**
- Async/await throughout (no blocking calls)
- Avoid unnecessary allocations
- Use `Cow<'_, str>` for strings that might be borrowed or owned
- Stream large result sets instead of loading into memory
**3. Error Handling Excellence**
- Rich error types using `thiserror`
- Context preservation through error chains
- Recoverable vs non-recoverable errors clearly distinguished
- Error types that enable caller decision-making
**4. API Ergonomics**
- Fluent builder APIs
- Method chaining where appropriate
- Smart defaults with explicit overrides
- Both owned and borrowed variants where it makes sense
**5. Documentation as Code**
- Every public item documented with examples
- Doc tests that actually compile and run
- Link to FlowGlad API docs in corresponding methods
---
## Architecture Overview
### Project Structure
```
flowglad-rs/
├── Cargo.toml
├── README.md
├── LICENSE
├── docs/
│ ├── IMPLEMENTATION_PLAN.md # This file
│ ├── ARCHITECTURE.md # Detailed architecture docs
│ └── CONTRIBUTING.md # Contribution guidelines
├── src/
│ ├── lib.rs # Public API surface
│ ├── client.rs # Core HTTP client
│ ├── config.rs # Client configuration
│ ├── error.rs # Error types
│ │
│ ├── resources/ # API resources (one file per resource)
│ │ ├── mod.rs
│ │ ├── customers.rs # Customer CRUD operations
│ │ ├── products.rs
│ │ ├── prices.rs
│ │ ├── subscriptions.rs
│ │ ├── checkout_sessions.rs
│ │ ├── invoices.rs
│ │ ├── invoice_line_items.rs
│ │ ├── payments.rs
│ │ ├── payment_methods.rs
│ │ ├── purchases.rs
│ │ ├── usage_meters.rs
│ │ ├── usage_events.rs
│ │ ├── features.rs
│ │ ├── product_features.rs
│ │ ├── subscription_item_features.rs
│ │ ├── discounts.rs
│ │ ├── pricing_models.rs
│ │ ├── webhooks.rs
│ │ └── api_keys.rs
│ │
│ ├── types/ # Data models and DTOs
│ │ ├── mod.rs
│ │ ├── ids.rs # Newtype wrappers for IDs
│ │ ├── customer.rs
│ │ ├── product.rs
│ │ ├── price.rs
│ │ ├── subscription.rs
│ │ ├── invoice.rs
│ │ ├── payment.rs
│ │ ├── usage.rs
│ │ ├── feature.rs
│ │ ├── discount.rs
│ │ ├── webhook.rs
│ │ └── common.rs # Shared types (Money, Currency, etc.)
│ │
│ └── utils/
│ ├── mod.rs
│ ├── pagination.rs # Pagination utilities
│ └── serde_helpers.rs # Custom serde implementations
│
├── examples/
│ ├── quickstart.rs
│ ├── create_customer.rs
│ ├── create_subscription.rs
│ ├── checkout_flow.rs
│ ├── usage_based_billing.rs
│ ├── handle_webhooks.rs
│ └── error_handling.rs
│
└── tests/
├── integration/
│ ├── mod.rs
│ ├── customers_tests.rs
│ ├── subscriptions_tests.rs
│ └── ...
├── mock_server.rs
└── fixtures/
└── *.json # Example API responses
```
---
## Implementation Phases
### Phase 0: Foundation (Start Here)
**Objective**: Set up the project structure and core dependencies
**Tasks**:
1. Update `Cargo.toml` with dependencies
2. Create directory structure
3. Set up basic error types
4. Create client configuration structure
**Key Files**:
- `Cargo.toml`
- `src/lib.rs`
- `src/error.rs`
- `src/config.rs`
**Impressive Techniques to Use**:
- Use `cargo-nextest` for faster testing
- Set up `cargo-deny` for dependency auditing
- Configure `rustfmt.toml` and `clippy.toml` for consistent style
- Use workspace if planning multiple crates
**Dependencies**:
```toml
[dependencies]
# HTTP client
reqwest = { version = "0.12", features = ["json", "stream"] }
tokio = { version = "1", features = ["full"] }
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Error handling
thiserror = "1"
anyhow = "1" # For examples/tests only
# Utilities
url = "2"
chrono = { version = "0.4", features = ["serde"] }
bytes = "1"
[dev-dependencies]
wiremock = "0.6" # Only for specific mock scenarios
tokio-test = "0.4"
pretty_assertions = "1"
once_cell = "1" # For lazy static test client
```
---
### Phase 1: Core Client & Authentication
**Objective**: Build the HTTP client with authentication and basic request/response handling
**Key Components**:
1. **Client Configuration** (`src/config.rs`)
```rust
#[derive(Debug, Clone)]
pub struct Config {
api_key: String,
base_url: Url,
timeout: Duration,
max_retries: u32,
}
impl Config {
pub fn new(api_key: impl Into<String>) -> Self {
}
pub fn builder() -> ConfigBuilder {
}
}
```
2. **HTTP Client** (`src/client.rs`)
```rust
pub struct Client {
http: reqwest::Client,
config: Config,
}
impl Client {
pub fn new(config: Config) -> Result<Self, Error> {
}
async fn request<T>(&self, req: Request) -> Result<T, Error>
where
T: DeserializeOwned,
{
}
}
```
3. **Error Types** (`src/error.rs`)
```rust
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("API error: {message} (status: {status})")]
Api {
status: StatusCode,
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Invalid configuration: {0}")]
Config(String),
#[error("Authentication failed: {0}")]
Authentication(String),
#[error("Rate limit exceeded, retry after {retry_after:?}")]
RateLimit {
retry_after: Option<Duration>,
},
}
pub type Result<T> = std::result::Result<T, Error>;
```
**Impressive Techniques**:
- Use `reqwest::RequestBuilder` internally but don't expose it
- Implement automatic retries with exponential backoff using `tower` middleware pattern
- Add request/response logging with `tracing` crate
- Support both environment variable and explicit API key configuration
- Use `secrecy` crate to protect API keys in memory
**Testing**:
- Mock HTTP client using `wiremock`
- Test authentication header injection
- Test retry logic with various error scenarios
- Test timeout handling
---
### Phase 2: Type System & Data Models
**Objective**: Define all data types with maximum type safety and ergonomics
**Key Components**:
1. **ID Newtypes** (`src/types/ids.rs`)
```rust
use serde::{Deserialize, Serialize};
use std::fmt;
macro_rules! define_id {
($name:ident) => {
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(String);
impl $name {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
};
}
define_id!(CustomerId);
define_id!(ProductId);
define_id!(PriceId);
define_id!(SubscriptionId);
define_id!(InvoiceId);
```
2. **Common Types** (`src/types/common.rs`)
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Money {
pub amount: i64,
pub currency: Currency,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Currency {
Usd,
Eur,
Gbp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Timestamp(#[serde(with = "chrono::serde::ts_seconds")] pub DateTime<Utc>);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageInfo {
pub has_more: bool,
pub total: Option<usize>,
}
```
3. **Resource Types** (e.g., `src/types/customer.rs`)
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Customer {
pub id: CustomerId,
pub email: Option<String>,
pub name: Option<String>,
pub metadata: HashMap<String, serde_json::Value>,
pub created_at: Timestamp,
pub updated_at: Timestamp,
}
#[derive(Debug, Default, Serialize)]
pub struct CreateCustomer {
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
impl CreateCustomer {
pub fn new() -> Self {
Self::default()
}
pub fn email(mut self, email: impl Into<String>) -> Self {
self.email = Some(email.into());
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.metadata.get_or_insert_with(HashMap::new)
.insert(key.into(), value);
self
}
}
```
**Impressive Techniques**:
- Use newtype pattern for all IDs to prevent mixing them up
- Implement `Display`, `AsRef<str>`, and other common traits
- Use `#[serde(skip_serializing_if = "Option::is_none")]` to avoid sending nulls
- Create builder types that return `Self` for fluent chaining
- Use `#[non_exhaustive]` on structs that might grow in the future
- Consider using `bon` crate for advanced builder patterns
- Use `derive_more` for common trait implementations
**Testing**:
- Test serialization/deserialization with real API response fixtures
- Use `insta` for snapshot testing of serialized output
- Test builder pattern with various combinations
---
### Phase 3: Resource Implementations (Iterative)
**Objective**: Implement each API resource as a separate module
**Priority Order** (implement in this order):
1. Customers (foundational)
2. Products & Prices (core billing)
3. Subscriptions (most common use case)
4. Checkout Sessions (user-facing)
5. Invoices & Payments (billing operations)
6. Usage Meters & Events (metered billing)
7. Features & Product Features (feature gating)
8. Discounts & Pricing Models (advanced)
9. Webhooks (integrations)
10. API Keys, Payment Methods, etc. (utilities)
**Resource Module Pattern** (e.g., `src/resources/customers.rs`):
```rust
use crate::{Client, Error, Result};
use crate::types::{Customer, CustomerId, CreateCustomer, UpdateCustomer, ListResponse};
/// Customer resource methods
impl Client {
/// Create a new customer
///
/// # Example
/// ```no_run
/// # use flowglad::{Client, Config};
/// # use flowglad::types::CreateCustomer;
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let client = Client::new(Config::new("sk_test_..."))?;
///
/// let customer = client.customers().create(
/// CreateCustomer::new()
/// .email("user@example.com")
/// .name("Jane Doe")
/// ).await?;
///
/// println!("Created customer: {}", customer.id);
/// # Ok(())
/// # }
/// ```
pub fn customers(&self) -> Customers {
Customers { client: self }
}
}
/// Customer resource methods
pub struct Customers<'a> {
client: &'a Client,
}
impl<'a> Customers<'a> {
/// Create a new customer
pub async fn create(&self, params: CreateCustomer) -> Result<Customer> {
self.client
.post("/customers")
.json(¶ms)
.send()
.await
}
/// Retrieve a customer by ID
pub async fn get(&self, id: &CustomerId) -> Result<Customer> {
self.client
.get(&format!("/customers/{}", id))
.send()
.await
}
/// List all customers
pub async fn list(&self) -> Result<ListResponse<Customer>> {
self.client
.get("/customers")
.send()
.await
}
/// List customers with pagination
pub fn list_paginated(&self) -> Paginator<Customer> {
Paginator::new(self.client, "/customers")
}
/// Update a customer
pub async fn update(&self, id: &CustomerId, params: UpdateCustomer) -> Result<Customer> {
self.client
.patch(&format!("/customers/{}", id))
.json(¶ms)
.send()
.await
}
/// Get billing details for a customer
pub async fn get_billing(&self, id: &CustomerId) -> Result<BillingDetails> {
self.client
.get(&format!("/customers/{}/billing", id))
.send()
.await
}
}
```
**Impressive Techniques**:
- Use "resource accessor" pattern: `client.customers().create(...)`
- Make resource structs borrow the client (zero allocation)
- Consistent naming: `create`, `get`, `list`, `update`, `delete`
- Provide both `.list()` and `.list_paginated()` variants
- Rich documentation with examples for every method
- Use lifetime annotations correctly to avoid unnecessary clones
**Testing**:
- One test file per resource
- Test happy path for each endpoint
- Test error cases
- Use fixtures for response mocking
---
### Phase 4: Advanced Features
#### 4.1 Pagination (`src/utils/pagination.rs`)
```rust
use futures::Stream;
use std::pin::Pin;
/// Paginated list response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListResponse<T> {
pub data: Vec<T>,
pub has_more: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
/// Async iterator for paginated results
pub struct Paginator<'a, T> {
client: &'a Client,
url: String,
cursor: Option<String>,
buffer: Vec<T>,
done: bool,
}
impl<'a, T> Paginator<'a, T>
where
T: DeserializeOwned + Unpin,
{
pub fn new(client: &'a Client, url: impl Into<String>) -> Self {
Self {
client,
url: url.into(),
cursor: None,
buffer: Vec::new(),
done: false,
}
}
/// Consume all pages and collect into a Vec
pub async fn collect_all(mut self) -> Result<Vec<T>> {
let mut all = Vec::new();
while let Some(item) = self.next().await.transpose()? {
all.push(item);
}
Ok(all)
}
}
impl<'a, T> Stream for Paginator<'a, T>
where
T: DeserializeOwned + Unpin,
{
type Item = Result<T>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
// Implement streaming pagination
// This is advanced - could start with simpler async iterator
todo!()
}
}
```
**Impressive Techniques**:
- Implement `Stream` trait for true async iteration
- Provide `.collect_all()` convenience method
- Handle cursor-based pagination automatically
- Consider using `async-stream` crate for simpler implementation
#### 4.2 Webhook Signature Verification
```rust
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
/// Verify a webhook signature
pub fn verify_webhook_signature(
payload: &[u8],
signature: &str,
secret: &str,
) -> Result<()> {
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
.map_err(|_| Error::Config("Invalid webhook secret".into()))?;
mac.update(payload);
let expected = hex::encode(mac.finalize().into_bytes());
if signature == expected {
Ok(())
} else {
Err(Error::Authentication("Invalid webhook signature".into()))
}
}
/// Parse and verify a webhook event
pub fn parse_webhook_event(
payload: &str,
signature: &str,
secret: &str,
) -> Result<WebhookEvent> {
verify_webhook_signature(payload.as_bytes(), signature, secret)?;
serde_json::from_str(payload).map_err(Into::into)
}
```
#### 4.3 Request Retry Logic
```rust
use tokio::time::{sleep, Duration};
pub struct RetryPolicy {
max_retries: u32,
initial_backoff: Duration,
max_backoff: Duration,
backoff_multiplier: f64,
}
impl RetryPolicy {
pub async fn execute<F, T, E>(&self, mut f: F) -> Result<T, E>
where
F: FnMut() -> Pin<Box<dyn Future<Output = Result<T, E>>>>,
E: IsRetryable,
{
let mut attempts = 0;
let mut backoff = self.initial_backoff;
loop {
match f().await {
Ok(val) => return Ok(val),
Err(e) if e.is_retryable() && attempts < self.max_retries => {
attempts += 1;
sleep(backoff).await;
backoff = (backoff * self.backoff_multiplier as u32)
.min(self.max_backoff);
}
Err(e) => return Err(e),
}
}
}
}
trait IsRetryable {
fn is_retryable(&self) -> bool;
}
impl IsRetryable for Error {
fn is_retryable(&self) -> bool {
matches!(self,
Error::Network(_) |
Error::RateLimit { .. } |
Error::Api { status, .. } if status.is_server_error()
)
}
}
```
---
### Phase 5: Testing Infrastructure
#### 5.1 Integration Testing with Real API
**IMPORTANT: Use Real FlowGlad Test API**
For integration tests, we will use the actual FlowGlad test API instead of a mock server. This ensures our SDK works correctly with the real API and catches any discrepancies between our implementation and FlowGlad's actual behavior.
**Test API Key** (use this for all integration tests):
```
sk_test_2UpNCeGvC1EuiJASnW2Z2i4Wot7N3goNdogBU4rLFnPqnp
```
**Integration Test Setup** (`tests/integration/mod.rs`):
```rust
use flowglad::{Client, Config};
use once_cell::sync::Lazy;
/// Shared test client using the FlowGlad test API key
pub static TEST_CLIENT: Lazy<Client> = Lazy::new(|| {
let config = Config::new("sk_test_2UpNCeGvC1EuiJASnW2Z2i4Wot7N3goNdogBU4rLFnPqnp");
Client::new(config).expect("Failed to create test client")
});
/// Helper to clean up test resources after a test
pub async fn cleanup_customer(client: &Client, id: &CustomerId) -> Result<()> {
// Note: You may need to implement delete methods or manually clean up
// test data in the FlowGlad dashboard
Ok(())
}
```
**Why Use Real API for Integration Tests?**
1. Catches real-world API behavior and edge cases
2. Validates request/response serialization with actual data
3. Tests authentication and error handling with real responses
4. Ensures compatibility with FlowGlad's latest API changes
5. More confidence in production readiness
**Test Organization:**
- Unit tests: Use in-module tests for pure functions and serialization
- Integration tests: Use the real API with the test key
- Mock tests: Only for specific scenarios where API calls aren't practical (e.g., rate limiting, network errors)
#### 5.2 Mock Server Setup (for specific scenarios)
For testing specific error scenarios or when you need full control over responses:
```rust
use wiremock::{MockServer, Mock, ResponseTemplate};
use wiremock::matchers::{method, path, header};
pub struct TestServer {
pub server: MockServer,
pub client: Client,
}
impl TestServer {
pub async fn new() -> Self {
let server = MockServer::start().await;
let config = Config::builder()
.api_key("sk_test_mock")
.base_url(server.uri())
.build();
let client = Client::new(config).unwrap();
Self { server, client }
}
pub fn mock_rate_limit_error(&self) -> Mock {
Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(429)
.insert_header("retry-after", "60")
.set_body_json(json!({
"error": "rate_limit_exceeded"
})))
}
}
```
#### 5.3 Integration Tests Pattern (with Real API)
```rust
use flowglad::types::CreateCustomer;
use crate::integration::TEST_CLIENT;
#[tokio::test]
async fn test_create_customer() {
// Using real FlowGlad test API
let result = TEST_CLIENT
.customers()
.create(
CreateCustomer::new()
.email("test@example.com")
.name("Test User")
)
.await;
assert!(result.is_ok());
let customer = result.unwrap();
assert_eq!(customer.email.unwrap(), "test@example.com");
assert_eq!(customer.name.unwrap(), "Test User");
// Clean up (if delete endpoint exists)
// cleanup_customer(&TEST_CLIENT, &customer.id).await.ok();
}
#[tokio::test]
async fn test_get_customer() {
// Create a customer first
let created = TEST_CLIENT
.customers()
.create(CreateCustomer::new().email("get-test@example.com"))
.await
.expect("Failed to create customer");
// Retrieve it
let retrieved = TEST_CLIENT
.customers()
.get(&created.id)
.await
.expect("Failed to get customer");
assert_eq!(created.id, retrieved.id);
assert_eq!(created.email, retrieved.email);
}
#[tokio::test]
async fn test_list_customers() {
let result = TEST_CLIENT
.customers()
.list()
.await;
assert!(result.is_ok());
let list = result.unwrap();
assert!(!list.data.is_empty());
}
#[tokio::test]
async fn test_error_handling() {
// Try to get a customer that doesn't exist
let result = TEST_CLIENT
.customers()
.get(&CustomerId::new("cus_nonexistent_invalid_id_12345"))
.await;
assert!(result.is_err());
match result.unwrap_err() {
Error::Api { status, .. } => assert_eq!(status, 404),
e => panic!("Expected API error, got: {:?}", e),
}
}
```
#### 5.4 Property-Based Testing
```rust
use proptest::prelude::*;
proptest! {
#[test]
fn test_customer_id_roundtrip(id in "cus_[a-zA-Z0-9]{10}") {
let customer_id = CustomerId::new(id.clone());
let serialized = serde_json::to_string(&customer_id).unwrap();
let deserialized: CustomerId = serde_json::from_str(&serialized).unwrap();
assert_eq!(customer_id, deserialized);
}
}
```
---
### Phase 6: Examples & Documentation
#### Example Structure Pattern
```rust
//! Create and manage customers
//!
//! This example demonstrates:
//! - Creating a customer
//! - Retrieving customer details
//! - Updating customer information
//! - Listing all customers
use flowglad::{Client, Config, Error};
use flowglad::types::CreateCustomer;
#[tokio::main]
async fn main() -> Result<(), Error> {
// Initialize the client
let client = Client::new(Config::from_env()?)?;
// Create a customer
let customer = client.customers()
.create(
CreateCustomer::new()
.email("jane@example.com")
.name("Jane Doe")
.metadata("plan", "premium")
)
.await?;
println!("Created customer: {}", customer.id);
// Retrieve the customer
let retrieved = client.customers()
.get(&customer.id)
.await?;
println!("Retrieved: {:#?}", retrieved);
Ok(())
}
```
#### Documentation Standards
Every public item should have:
1. Brief one-line summary
2. Detailed description if needed
3. Code example showing usage
4. Link to FlowGlad API docs
5. Notes about edge cases or important behavior
```rust
/// Create a new customer in FlowGlad.
///
/// This method creates a customer record that can be associated with
/// subscriptions, invoices, and payments. Email is optional but recommended
/// for sending invoices and receipts.
///
/// # Arguments
///
/// * `params` - Customer creation parameters
///
/// # Example
///
/// ```no_run
/// # use flowglad::{Client, Config};
/// # use flowglad::types::CreateCustomer;
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// # let client = Client::new(Config::new("sk_test_..."))?;
/// let customer = client.customers().create(
/// CreateCustomer::new()
/// .email("user@example.com")
/// .name("Jane Doe")
/// ).await?;
/// # Ok(())
/// # }
/// ```
///
/// # Errors
///
/// Returns `Error::Api` if the API request fails, for example if the
/// email is already in use.
///
/// # See Also
///
/// - [FlowGlad API: Create Customer](https://docs.flowglad.com/api-reference/customer/create-customer)
pub async fn create(&self, params: CreateCustomer) -> Result<Customer> {
// ...
}
```
---
## Code Quality Guidelines
### Rust Idioms to Follow
1. **Prefer borrowing over cloning**
```rust
pub async fn get(&self, id: &CustomerId) -> Result<Customer>
pub async fn get(&self, id: CustomerId) -> Result<Customer>
```
2. **Use `impl Trait` for flexibility**
```rust
pub fn name(mut self, name: impl Into<String>) -> Self
pub fn name(mut self, name: String) -> Self
```
3. **Leverage type inference**
```rust
let customers = client.customers().list().await?;
let customers: ListResponse<Customer> = client.customers().list().await?;
```
4. **Use `derive` macros appropriately**
```rust
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Customer { ... }
```
5. **Non-exhaustive for future compatibility**
```rust
#[non_exhaustive]
pub struct Config { ... }
#[non_exhaustive]
pub enum Error { ... }
```
### Performance Considerations
1. **Avoid unnecessary allocations**
- Use `&str` in function parameters
- Use `Cow<'_, str>` when you might not need to allocate
- Consider `Arc<str>` for shared immutable strings
2. **Use zero-copy deserialization**
```rust
#[derive(Deserialize)]
pub struct Customer<'a> {
#[serde(borrow)]
pub email: Cow<'a, str>,
}
```
3. **Stream large responses**
- Don't load all results into memory
- Use `Stream` for pagination
- Consider `async_stream!` macro for easier implementation
4. **Connection pooling**
- `reqwest::Client` already pools connections
- Reuse the same client instance
### Error Handling Best Practices
1. **Preserve context**
```rust
.map_err(|e| Error::Config(format!("Failed to parse URL: {}", e)))?
```
2. **Don't swallow errors**
- Always propagate or log errors
- Use `#[source]` in error types for chains
3. **Make errors actionable**
- Include enough information for the caller to decide what to do
- Distinguish between retryable and non-retryable errors
### API Design Principles
1. **Principle of Least Surprise**
- Follow stdlib conventions
- Match patterns from popular crates (tokio, reqwest, etc.)
2. **Progressive Disclosure**
- Simple things should be simple
- Complex things should be possible
- Don't expose internal complexity
3. **Type Safety**
- Use newtypes to prevent mistakes
- Use enums instead of strings where possible
- Leverage the type system to catch errors at compile time
4. **Async-First**
- All IO operations are async
- No blocking calls in the library
- Consider providing blocking wrappers if needed
---
## Testing Strategy
### Test Categories
1. **Unit Tests** (in module files)
- Test individual functions
- Test serialization/deserialization
- Test builders and constructors
2. **Integration Tests** (in `tests/` directory)
- Test API calls with mock server
- Test error handling
- Test pagination
- Test retry logic
3. **Documentation Tests**
- Every example in docs must compile
- Use `# ` prefix for hidden setup lines
- Use `no_run` when the test requires network
4. **Snapshot Tests** (with `insta` crate)
- Test JSON serialization matches expected format
- Useful for catching unintended changes
### Test Utilities to Create
```rust
// tests/common/mod.rs
pub mod fixtures {
use std::fs;
pub fn load_fixture(name: &str) -> String {
fs::read_to_string(format!("tests/fixtures/{}.json", name))
.expect("fixture file not found")
}
pub fn customer_fixture() -> Customer {
serde_json::from_str(&load_fixture("customer")).unwrap()
}
}
pub mod assertions {
pub fn assert_customer_eq(a: &Customer, b: &Customer) {
assert_eq!(a.id, b.id);
assert_eq!(a.email, b.email);
assert_eq!(a.name, b.name);
// Don't compare timestamps as they might differ
}
}
```
### Coverage Goals
- Aim for >80% code coverage
- 100% coverage for error handling paths
- All public APIs must have examples
- All examples must compile
---
## Future Considerations
### Phase 7: Advanced Features (Future)
1. **Webhook Server Helpers**
- Axum/Actix integration
- Automatic signature verification
- Type-safe event handling
2. **Rate Limiting**
- Respect rate limit headers
- Automatic backoff
- Circuit breaker pattern
3. **Caching Layer**
- Optional caching for GET requests
- Cache invalidation on mutations
- Pluggable cache backends
4. **Logging & Observability**
- Request/response logging with `tracing`
- OpenTelemetry integration
- Metrics collection
5. **CLI Tool**
- Interactive CLI using `clap`
- Config file management
- Quick testing of API calls
### Extensibility Points
1. **Middleware System**
- Allow users to inject custom logic
- Request/response transformation
- Custom authentication schemes
2. **Plugin System**
- Custom serializers
- Custom ID types
- Custom error handlers
### Maintenance Considerations
1. **Semantic Versioning**
- Follow SemVer strictly
- Document breaking changes
- Provide migration guides
2. **API Stability**
- Use `#[non_exhaustive]` for future additions
- Deprecation warnings before removal
- Maintain compatibility shims when possible
3. **Dependency Management**
- Keep dependencies minimal
- Use `cargo-deny` to audit
- Document MSRV (Minimum Supported Rust Version)
---
## Implementation Checklist
Use this checklist when implementing each phase:
### Foundation Checklist
- [ ] Project structure created
- [ ] Dependencies added to Cargo.toml
- [ ] Basic error types defined
- [ ] Config structure implemented
- [ ] README with quickstart example
- [ ] CI/CD setup (GitHub Actions)
### Core Client Checklist
- [ ] HTTP client with authentication
- [ ] Request/response handling
- [ ] Error handling with rich types
- [ ] Retry logic with backoff
- [ ] Timeout handling
- [ ] Environment variable support
- [ ] Unit tests for client
- [ ] Integration tests with mock server
### Per-Resource Checklist
- [ ] Data types defined (types/resource.rs)
- [ ] Builder types for mutations
- [ ] Resource methods (resources/resource.rs)
- [ ] Documentation with examples
- [ ] Unit tests for types
- [ ] Integration tests for methods
- [ ] Example program
### Polish Checklist
- [ ] All public APIs documented
- [ ] All examples compile and run
- [ ] Error messages are helpful
- [ ] README is comprehensive
- [ ] CHANGELOG maintained
- [ ] Code formatted with rustfmt
- [ ] No clippy warnings
- [ ] CI passes
- [ ] Coverage >80%
---
## Tips for Future You
### When Adding New Endpoints
1. Start with the data types (request/response)
2. Add the method to the resource struct
3. Write a test first (TDD approach)
4. Implement the method
5. Add documentation with example
6. Add an example program if it's a complex flow
### When Debugging
1. Check wiremock expectations - are you testing what you think?
2. Use `dbg!()` macro liberally during development
3. Enable `reqwest` debug logging: `RUST_LOG=reqwest=debug`
4. Pretty-print JSON: `serde_json::to_string_pretty()`
### When Stuck
1. Look at how other Rust SDKs do it:
- `stripe-rust`
- `aws-sdk-rust`
- `octocrab` (GitHub API)
- `twilight` (Discord API)
2. Review Rust API guidelines: https://rust-lang.github.io/api-guidelines/
3. Ask for help:
- Rust Discord
- StackOverflow
- r/rust
### Common Pitfalls to Avoid
1. **Don't block the executor**
- Never use `std::thread::sleep` in async code
- Use `tokio::time::sleep` instead
2. **Don't clone unnecessarily**
- Pass references when possible
- Use `Arc` for shared ownership, not `clone()`
3. **Don't ignore errors**
- Every `Result` should be handled
- Use `allow(unused_must_use)` only if you really mean it
4. **Don't make the API too generic**
- Balance flexibility with simplicity
- Not everything needs to be generic
5. **Don't forget about backwards compatibility**
- Use `#[non_exhaustive]`
- Deprecate before removing
- Follow SemVer
---
## Impressive Rust Patterns to Use
### 1. Typestate Pattern for Builders
```rust
// Builder that enforces required fields at compile time
pub struct CustomerBuilder<Email> {
email: Email,
name: Option<String>,
}
pub struct NoEmail;
pub struct HasEmail(String);
impl CustomerBuilder<NoEmail> {
pub fn new() -> Self {
Self {
email: NoEmail,
name: None,
}
}
pub fn email(self, email: impl Into<String>) -> CustomerBuilder<HasEmail> {
CustomerBuilder {
email: HasEmail(email.into()),
name: self.name,
}
}
}
impl CustomerBuilder<HasEmail> {
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn build(self) -> CreateCustomer {
CreateCustomer {
email: self.email.0,
name: self.name,
}
}
}
// Usage: won't compile without email
let customer = CustomerBuilder::new()
.email("test@example.com") // Required
.name("Test") // Optional
.build();
```
### 2. Sealed Trait Pattern
```rust
mod sealed {
pub trait Sealed {}
}
pub trait Resource: sealed::Sealed {
type Id;
const ENDPOINT: &'static str;
}
impl sealed::Sealed for Customer {}
impl Resource for Customer {
type Id = CustomerId;
const ENDPOINT: &'static str = "/customers";
}
// Now you can write generic code over resources
impl Client {
async fn get_resource<R: Resource>(&self, id: &R::Id) -> Result<R> {
// Generic implementation
}
}
```
### 3. Extension Traits for Ergonomics
```rust
pub trait ResultExt<T> {
fn context(self, msg: impl Into<String>) -> Result<T>;
}
impl<T, E: Into<Error>> ResultExt<T> for Result<T, E> {
fn context(self, msg: impl Into<String>) -> Result<T> {
self.map_err(|e| {
let msg = msg.into();
Error::Context {
message: msg,
source: Box::new(e.into()),
}
})
}
}
// Usage
let config = Config::from_env()
.context("Failed to load configuration")?;
```
### 4. Smart Pointer Pattern for IDs
```rust
use std::sync::Arc;
#[derive(Clone)]
pub struct CustomerId(Arc<str>);
impl CustomerId {
pub fn new(id: impl Into<String>) -> Self {
Self(Arc::from(id.into()))
}
}
// Cloning is cheap (just Arc increment)
// But still have type safety
```
---
## Resources & References
### Essential Reading
- [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/)
- [The Async Book](https://rust-lang.github.io/async-book/)
- [tokio Tutorial](https://tokio.rs/tokio/tutorial)
### Inspirational Crates
- `stripe-rust` - Well-designed payment SDK
- `octocrab` - Clean GitHub API client
- `reqwest` - Industry-standard HTTP client design
- `sqlx` - Async database with great error handling
### Tools
- `cargo-nextest` - Faster test runner
- `cargo-deny` - Dependency auditing
- `cargo-outdated` - Check for updates
- `cargo-audit` - Security vulnerabilities
- `insta` - Snapshot testing
- `wiremock` - HTTP mocking
---
## Success Criteria
This SDK will be considered successful when:
1. **For Users**
- Can get started in <5 minutes
- Intuitive API that follows Rust conventions
- Helpful error messages
- Works out of the box
2. **For Maintainers**
- Easy to add new endpoints
- Tests catch regressions
- CI catches issues before merge
- Clear contribution guidelines
3. **For Rust Experts**
- Uses advanced patterns appropriately
- Type system used to prevent errors
- Zero-cost abstractions
- Idiomatic Rust throughout
- Source code is a learning resource
---
## Final Notes
Remember: **Clear and simple doesn't mean simplistic.** The goal is to create an SDK that:
- Is easy to use for beginners
- Powerful enough for experts
- Demonstrates Rust best practices
- Makes incorrect usage hard to write
- Makes correct usage easy to write
Start simple, iterate, and refine. Don't try to implement everything at once. Get one resource working perfectly, then use it as a template for others.
Good luck! 🦀