Crate reso_client

Crate reso_client 

Source
Expand description

RESO Web API Client Library

A Rust client library for RESO Web API servers that implement the OData 4.0 protocol. This library provides a type-safe, ergonomic interface for querying real estate data from MLS systems.

§Features

  • 🔍 Fluent Query Builder - Build complex OData queries with a clean, fluent API
  • 🔐 OAuth Authentication - Built-in support for bearer token authentication
  • 📊 Full OData Support - Filter, sort, paginate, select fields, expand relations
  • 🔢 Count Queries - Efficient record counting via /$count endpoint
  • 🗂️ Dataset ID Support - Handle RESO servers that use dataset identifiers
  • 📖 Metadata Retrieval - Fetch and parse OData $metadata documents
  • 🔄 Replication Endpoint - Bulk data transfer with up to 2000 records/request
  • Async/Await - Built on tokio for high-performance concurrent operations
  • 🛡️ Type-Safe Errors - Comprehensive error types with detailed context

§Stability

This library is pre-1.0, meaning the API may change between minor versions. It follows semantic versioning, but breaking changes may occur in 0.x releases.

§Quick Start

use reso_client::{ResoClient, QueryBuilder};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create client from environment variables
    // Requires: RESO_BASE_URL and RESO_TOKEN
    let client = ResoClient::from_env()?;

    // Build and execute a query
    let query = QueryBuilder::new("Property")
        .filter("City eq 'Austin' and ListPrice gt 500000")
        .select(&["ListingKey", "City", "ListPrice", "BedroomsTotal"])
        .order_by("ListPrice", "desc")
        .top(10)
        .build()?;

    let results = client.execute(&query).await?;

    // Access the records from the OData response
    if let Some(records) = results["value"].as_array() {
        println!("Found {} properties", records.len());
        for record in records {
            let key = record["ListingKey"].as_str().unwrap_or("");
            let price = record["ListPrice"].as_f64().unwrap_or(0.0);
            println!("{}: ${}", key, price);
        }
    }

    Ok(())
}

§Configuration

The client can be configured via environment variables or programmatically:

§Environment Variables

RESO_BASE_URL=https://api.bridgedataoutput.com/api/v2/OData
RESO_TOKEN=your-oauth-token
RESO_DATASET_ID=actris_ref  # Optional
RESO_TIMEOUT=30              # Optional, seconds

§Manual Configuration

let config = ClientConfig::new(
    "https://api.mls.com/odata",
    "your-bearer-token"
)
.with_dataset_id("actris_ref")
.with_timeout(Duration::from_secs(60));

let client = ResoClient::with_config(config)?;

§Common Usage Patterns

§Filtering with OData Expressions

// Simple equality
let query = QueryBuilder::new("Property")
    .filter("City eq 'Austin'")
    .build()?;

// Multiple conditions
let query = QueryBuilder::new("Property")
    .filter("City eq 'Austin' and ListPrice gt 500000 and BedroomsTotal ge 3")
    .build()?;

// String functions
let query = QueryBuilder::new("Property")
    .filter("startswith(City, 'San')")
    .build()?;

// Date comparison
let query = QueryBuilder::new("Property")
    .filter("ModificationTimestamp gt 2025-01-01T00:00:00Z")
    .build()?;

§Pagination

// First page
let query = QueryBuilder::new("Property")
    .top(20)
    .build()?;
let page1 = client.execute(&query).await?;

// Second page
let query = QueryBuilder::new("Property")
    .skip(20)
    .top(20)
    .build()?;
let page2 = client.execute(&query).await?;

§Getting Total Counts

// Include count in response (with records)
let query = QueryBuilder::new("Property")
    .filter("City eq 'Austin'")
    .with_count()
    .top(10)
    .build()?;

let results = client.execute(&query).await?;
if let Some(count) = results["@odata.count"].as_u64() {
    println!("Total matching records: {}", count);
}

// Count only (no records, more efficient)
let query = QueryBuilder::new("Property")
    .filter("City eq 'Austin'")
    .count()
    .build()?;

let count = client.execute_count(&query).await?;
println!("Total: {}", count);

§Bulk Data with Replication

let query = ReplicationQueryBuilder::new("Property")
    .filter("StandardStatus eq 'Active'")
    .select(&["ListingKey", "City", "ListPrice"])
    .top(2000)  // Max: 2000 for replication
    .build()?;

let mut response = client.execute_replication(&query).await?;
let mut all_records = response.records;

// Continue fetching while next link is available
while let Some(next_link) = response.next_link {
    response = client.execute_next_link(&next_link).await?;
    all_records.extend(response.records);
}

println!("Total records fetched: {}", all_records.len());

§Error Handling

let client = ResoClient::from_env()?;
let query = QueryBuilder::new("Property").top(10).build()?;

match client.execute(&query).await {
    Ok(results) => {
        println!("Success!");
    }
    Err(ResoError::Unauthorized { message, .. }) => {
        eprintln!("Authentication failed: {}", message);
    }
    Err(ResoError::NotFound { message, .. }) => {
        eprintln!("Resource not found: {}", message);
    }
    Err(ResoError::RateLimited { message, .. }) => {
        eprintln!("Rate limited: {}", message);
    }
    Err(ResoError::Network(msg)) => {
        eprintln!("Network error: {}", msg);
    }
    Err(e) => {
        eprintln!("Other error: {}", e);
    }
}

§Stability

This library is pre-1.0, meaning the API may change between minor versions. We follow semantic versioning, but breaking changes may occur in 0.x releases. We strive to keep changes minimal and well-documented in the CHANGELOG.

§Resources

Re-exports§

pub use client::ClientConfig;
pub use client::ResoClient;
pub use error::ResoError;
pub use error::Result;
pub use queries::Query;
pub use queries::QueryBuilder;
pub use queries::ReplicationQuery;
pub use queries::ReplicationQueryBuilder;
pub use replication::ReplicationResponse;

Modules§

client
Client configuration and connection management
error
Error types for the RESO client library
queries
Query building for RESO/OData requests
replication
Replication endpoint response types

Enums§

JsonValue
Represents any valid JSON value.