reso_client/lib.rs
1// src/lib.rs
2
3//! RESO Web API Client Library
4//!
5//! A Rust client library for [RESO Web API](https://www.reso.org/reso-web-api/)
6//! servers that implement the OData 4.0 protocol. This library provides a type-safe,
7//! ergonomic interface for querying real estate data from MLS systems.
8//!
9//! # Features
10//!
11//! - 🔍 **Fluent Query Builder** - Build complex OData queries with a clean, fluent API
12//! - 🔐 **OAuth Authentication** - Built-in support for bearer token authentication
13//! - 📊 **Full OData Support** - Filter, sort, paginate, select fields, expand relations
14//! - 🔢 **Count Queries** - Efficient record counting via `/$count` endpoint
15//! - 🗂️ **Dataset ID Support** - Handle RESO servers that use dataset identifiers
16//! - 📖 **Metadata Retrieval** - Fetch and parse OData `$metadata` documents
17//! - 🔄 **Replication Endpoint** - Bulk data transfer with up to 2000 records/request
18//! - ⚡ **Async/Await** - Built on tokio for high-performance concurrent operations
19//! - 🛡️ **Type-Safe Errors** - Comprehensive error types with detailed context
20//!
21//! ## Stability
22//!
23//! This library is pre-1.0, meaning the API may change between minor versions.
24//! It follows semantic versioning, but breaking changes may occur in 0.x releases.
25//!
26//! # Quick Start
27//!
28//! ```no_run
29//! use reso_client::{ResoClient, QueryBuilder};
30//!
31//! #[tokio::main]
32//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
33//! // Create client from environment variables
34//! // Requires: RESO_BASE_URL and RESO_TOKEN
35//! let client = ResoClient::from_env()?;
36//!
37//! // Build and execute a query
38//! let query = QueryBuilder::new("Property")
39//! .filter("City eq 'Austin' and ListPrice gt 500000")
40//! .select(&["ListingKey", "City", "ListPrice", "BedroomsTotal"])
41//! .order_by("ListPrice", "desc")
42//! .top(10)
43//! .build()?;
44//!
45//! let results = client.execute(&query).await?;
46//!
47//! // Access the records from the OData response
48//! if let Some(records) = results["value"].as_array() {
49//! println!("Found {} properties", records.len());
50//! for record in records {
51//! let key = record["ListingKey"].as_str().unwrap_or("");
52//! let price = record["ListPrice"].as_f64().unwrap_or(0.0);
53//! println!("{}: ${}", key, price);
54//! }
55//! }
56//!
57//! Ok(())
58//! }
59//! ```
60//!
61//! # Configuration
62//!
63//! The client can be configured via environment variables or programmatically:
64//!
65//! ## Environment Variables
66//!
67//! ```bash
68//! RESO_BASE_URL=https://api.bridgedataoutput.com/api/v2/OData
69//! RESO_TOKEN=your-oauth-token
70//! RESO_DATASET_ID=actris_ref # Optional
71//! RESO_TIMEOUT=30 # Optional, seconds
72//! ```
73//!
74//! ## Manual Configuration
75//!
76//! ```no_run
77//! # use reso_client::{ResoClient, ClientConfig};
78//! # use std::time::Duration;
79//! let config = ClientConfig::new(
80//! "https://api.mls.com/odata",
81//! "your-bearer-token"
82//! )
83//! .with_dataset_id("actris_ref")
84//! .with_timeout(Duration::from_secs(60));
85//!
86//! let client = ResoClient::with_config(config)?;
87//! # Ok::<(), Box<dyn std::error::Error>>(())
88//! ```
89//!
90//! # Common Usage Patterns
91//!
92//! ## Filtering with OData Expressions
93//!
94//! ```no_run
95//! # use reso_client::{ResoClient, QueryBuilder};
96//! # async fn example(client: &ResoClient) -> Result<(), Box<dyn std::error::Error>> {
97//! // Simple equality
98//! let query = QueryBuilder::new("Property")
99//! .filter("City eq 'Austin'")
100//! .build()?;
101//!
102//! // Multiple conditions
103//! let query = QueryBuilder::new("Property")
104//! .filter("City eq 'Austin' and ListPrice gt 500000 and BedroomsTotal ge 3")
105//! .build()?;
106//!
107//! // String functions
108//! let query = QueryBuilder::new("Property")
109//! .filter("startswith(City, 'San')")
110//! .build()?;
111//!
112//! // Date comparison
113//! let query = QueryBuilder::new("Property")
114//! .filter("ModificationTimestamp gt 2025-01-01T00:00:00Z")
115//! .build()?;
116//! # Ok(())
117//! # }
118//! ```
119//!
120//! ## Pagination
121//!
122//! ```no_run
123//! # use reso_client::{ResoClient, QueryBuilder};
124//! # async fn example(client: &ResoClient) -> Result<(), Box<dyn std::error::Error>> {
125//! // First page
126//! let query = QueryBuilder::new("Property")
127//! .top(20)
128//! .build()?;
129//! let page1 = client.execute(&query).await?;
130//!
131//! // Second page
132//! let query = QueryBuilder::new("Property")
133//! .skip(20)
134//! .top(20)
135//! .build()?;
136//! let page2 = client.execute(&query).await?;
137//! # Ok(())
138//! # }
139//! ```
140//!
141//! ## Getting Total Counts
142//!
143//! ```no_run
144//! # use reso_client::{ResoClient, QueryBuilder};
145//! # async fn example(client: &ResoClient) -> Result<(), Box<dyn std::error::Error>> {
146//! // Include count in response (with records)
147//! let query = QueryBuilder::new("Property")
148//! .filter("City eq 'Austin'")
149//! .with_count()
150//! .top(10)
151//! .build()?;
152//!
153//! let results = client.execute(&query).await?;
154//! if let Some(count) = results["@odata.count"].as_u64() {
155//! println!("Total matching records: {}", count);
156//! }
157//!
158//! // Count only (no records, more efficient)
159//! let query = QueryBuilder::new("Property")
160//! .filter("City eq 'Austin'")
161//! .count()
162//! .build()?;
163//!
164//! let count = client.execute_count(&query).await?;
165//! println!("Total: {}", count);
166//! # Ok(())
167//! # }
168//! ```
169//!
170//! ## Bulk Data with Replication
171//!
172//! ```no_run
173//! # use reso_client::{ResoClient, ReplicationQueryBuilder};
174//! # async fn example(client: &ResoClient) -> Result<(), Box<dyn std::error::Error>> {
175//! let query = ReplicationQueryBuilder::new("Property")
176//! .filter("StandardStatus eq 'Active'")
177//! .select(&["ListingKey", "City", "ListPrice"])
178//! .top(2000) // Max: 2000 for replication
179//! .build()?;
180//!
181//! let mut response = client.execute_replication(&query).await?;
182//! let mut all_records = response.records;
183//!
184//! // Continue fetching while next link is available
185//! while let Some(next_link) = response.next_link {
186//! response = client.execute_next_link(&next_link).await?;
187//! all_records.extend(response.records);
188//! }
189//!
190//! println!("Total records fetched: {}", all_records.len());
191//! # Ok(())
192//! # }
193//! ```
194//!
195//! ## Error Handling
196//!
197//! ```no_run
198//! # use reso_client::{ResoClient, QueryBuilder, ResoError};
199//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
200//! let client = ResoClient::from_env()?;
201//! let query = QueryBuilder::new("Property").top(10).build()?;
202//!
203//! match client.execute(&query).await {
204//! Ok(results) => {
205//! println!("Success!");
206//! }
207//! Err(ResoError::Unauthorized { message, .. }) => {
208//! eprintln!("Authentication failed: {}", message);
209//! }
210//! Err(ResoError::NotFound { message, .. }) => {
211//! eprintln!("Resource not found: {}", message);
212//! }
213//! Err(ResoError::RateLimited { message, .. }) => {
214//! eprintln!("Rate limited: {}", message);
215//! }
216//! Err(ResoError::Network(msg)) => {
217//! eprintln!("Network error: {}", msg);
218//! }
219//! Err(e) => {
220//! eprintln!("Other error: {}", e);
221//! }
222//! }
223//! # Ok(())
224//! # }
225//! ```
226//!
227//! # Stability
228//!
229//! This library is pre-1.0, meaning the API may change between minor versions.
230//! We follow semantic versioning, but breaking changes may occur in 0.x releases.
231//! We strive to keep changes minimal and well-documented in the CHANGELOG.
232//!
233//! # Resources
234//!
235//! - [RESO Web API Specification](https://www.reso.org/reso-web-api/)
236//! - [OData 4.0 Protocol](https://www.odata.org/documentation/)
237//! - [RESO Data Dictionary](https://www.reso.org/data-dictionary/)
238
239pub mod client;
240pub mod error;
241pub mod queries;
242pub mod replication;
243
244// Re-export main types for convenience
245pub use client::{ClientConfig, ResoClient};
246pub use error::{ResoError, Result};
247pub use queries::{Query, QueryBuilder, ReplicationQuery, ReplicationQueryBuilder};
248pub use replication::ReplicationResponse;
249
250// Re-export serde_json for convenience
251pub use serde_json::Value as JsonValue;