use std::sync::Arc;
use cratestack_codec_cbor::CborCodec;
use crate::auth::RequestAuthorizer;
use crate::codec::HttpClientCodec;
use crate::config::ClientConfig;
use crate::error::ClientError;
use crate::state::{ClientStateStore, InMemoryStateStore, PersistedClientState};
fn ensure_crypto_provider() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
#[derive(Clone)]
pub struct CratestackClient<C = CborCodec> {
pub(crate) http: reqwest::Client,
pub(crate) config: ClientConfig,
pub(crate) codec: C,
pub(crate) state_store: Arc<dyn ClientStateStore>,
pub(crate) request_authorizer: Option<Arc<dyn RequestAuthorizer>>,
pub(crate) schema_sha: Option<&'static str>,
}
impl CratestackClient<CborCodec> {
pub fn cbor(config: ClientConfig) -> Self {
Self::new(config, CborCodec)
}
}
impl<C> CratestackClient<C>
where
C: HttpClientCodec,
{
pub fn new(config: ClientConfig, codec: C) -> Self {
ensure_crypto_provider();
Self {
http: reqwest::Client::new(),
config,
codec,
state_store: Arc::new(InMemoryStateStore::default()),
request_authorizer: None,
schema_sha: None,
}
}
pub fn with_http_client(config: ClientConfig, codec: C, http: reqwest::Client) -> Self {
Self {
http,
config,
codec,
state_store: Arc::new(InMemoryStateStore::default()),
request_authorizer: None,
schema_sha: None,
}
}
pub fn with_state_store(mut self, state_store: Arc<dyn ClientStateStore>) -> Self {
self.state_store = state_store;
self
}
pub fn with_optional_state_store(self, state_store: Option<Arc<dyn ClientStateStore>>) -> Self {
match state_store {
Some(state_store) => self.with_state_store(state_store),
None => self,
}
}
pub fn with_request_authorizer(
mut self,
request_authorizer: Arc<dyn RequestAuthorizer>,
) -> Self {
self.request_authorizer = Some(request_authorizer);
self
}
pub fn with_schema_sha(mut self, schema_sha: &'static str) -> Self {
self.schema_sha = Some(schema_sha);
self
}
pub fn state(&self) -> Result<PersistedClientState, ClientError> {
self.state_store
.load()
.map_err(|error| ClientError::State(error.to_string()))
}
}
#[cfg(test)]
pub(crate) mod tests {
use cratestack_core::CoolError;
use super::*;
#[derive(Debug, Default)]
pub(crate) struct FailingStateStore;
impl ClientStateStore for FailingStateStore {
fn load(&self) -> Result<PersistedClientState, CoolError> {
Err(CoolError::Internal(
"simulated state store failure".to_owned(),
))
}
fn save(&self, _state: &PersistedClientState) -> Result<(), CoolError> {
Err(CoolError::Internal(
"simulated state store failure".to_owned(),
))
}
}
#[test]
fn state_store_error_maps_to_client_error_state() {
let client = CratestackClient::cbor(ClientConfig::new(
"http://example.invalid".parse().expect("valid url"),
))
.with_state_store(Arc::new(FailingStateStore));
let error = client.state().expect_err("state store is rigged to fail");
match error {
ClientError::State(message) => {
assert!(message.contains("simulated state store failure"));
}
other => panic!("expected ClientError::State for a state-store failure, got {other:?}"),
}
}
}