Skip to main content

polyoxide_core/
lib.rs

1//! # polyoxide-core
2//!
3//! Core utilities and shared types for Polyoxide Polymarket API clients.
4//!
5//! This crate provides common functionality used across `polyoxide-clob`, `polyoxide-gamma`, and `polyoxide-data`:
6//! - Shared error types and error handling
7//! - HTTP client configuration
8//! - Request builder utilities
9//!
10//! ## HTTP Client
11//!
12//! Use [`HttpClientBuilder`] to create configured HTTP clients:
13//!
14//! ```
15//! use polyoxide_core::HttpClientBuilder;
16//!
17//! let client = HttpClientBuilder::new("https://api.example.com")
18//!     .timeout_ms(60_000)
19//!     .build()
20//!     .unwrap();
21//! ```
22//!
23//! ## Error Handling
24//!
25//! Use the [`impl_api_error_conversions`] macro to reduce boilerplate in error types.
26
27#[macro_use]
28pub mod macros;
29
30pub mod auth;
31pub mod client;
32pub mod error;
33pub mod rate_limit;
34pub mod request;
35
36#[cfg(feature = "keychain")]
37pub mod keychain;
38
39/// Maximum number of characters to include in log messages containing response bodies.
40const LOG_BODY_MAX_LEN: usize = 512;
41
42/// Truncate a string for safe inclusion in log output.
43///
44/// Returns the original string if it fits within `LOG_BODY_MAX_LEN`,
45/// otherwise truncates at a UTF-8 boundary and appends `... [truncated]`.
46pub fn truncate_for_log(s: &str) -> std::borrow::Cow<'_, str> {
47    if s.len() <= LOG_BODY_MAX_LEN {
48        std::borrow::Cow::Borrowed(s)
49    } else {
50        let truncated = &s[..s.floor_char_boundary(LOG_BODY_MAX_LEN)];
51        std::borrow::Cow::Owned(format!("{}... [truncated]", truncated))
52    }
53}
54
55pub use auth::{current_timestamp, Base64Format, Signer};
56pub use client::{
57    retry_after_header, HttpClient, HttpClientBuilder, DEFAULT_POOL_SIZE, DEFAULT_TIMEOUT_MS,
58};
59pub use error::ApiError;
60pub use rate_limit::{RateLimiter, RetryConfig};
61pub use request::{QueryBuilder, Request, RequestError};
62
63#[cfg(feature = "keychain")]
64pub use keychain::KeychainError;
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn test_truncate_for_log_short_string_unchanged() {
72        let short = "hello world";
73        let result = truncate_for_log(short);
74        assert_eq!(result.as_ref(), short);
75    }
76
77    #[test]
78    fn test_truncate_for_log_exact_limit_unchanged() {
79        let exact = "a".repeat(LOG_BODY_MAX_LEN);
80        let result = truncate_for_log(&exact);
81        assert_eq!(result.as_ref(), exact.as_str());
82    }
83
84    #[test]
85    fn test_truncate_for_log_over_limit_truncated() {
86        let long = "x".repeat(LOG_BODY_MAX_LEN + 100);
87        let result = truncate_for_log(&long);
88        assert!(result.ends_with("... [truncated]"));
89        assert!(result.len() < long.len());
90    }
91
92    #[test]
93    fn test_truncate_for_log_multibyte_char_boundary() {
94        // Create a string where the 512th byte falls inside a multi-byte char
95        let mut s = "a".repeat(LOG_BODY_MAX_LEN - 1);
96        s.push('\u{1F600}'); // 4-byte emoji at position 511-514
97        s.push_str("overflow");
98        let result = truncate_for_log(&s);
99        assert!(result.ends_with("... [truncated]"));
100        // Should not panic or produce invalid UTF-8
101        assert!(result.is_char_boundary(0));
102    }
103}