Skip to content

Commit da185b8

Browse files
committed
Split client into a cached and uncached client
Cached client uses the uncached client internally too.
1 parent 623d78a commit da185b8

File tree

6 files changed

+218
-101
lines changed

6 files changed

+218
-101
lines changed

crates/rust-releases-io/src/client.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ use crate::document::RetrievedDocument;
33
#[cfg(feature = "http_client")]
44
pub mod cached_client;
55

6+
#[cfg(feature = "http_client")]
7+
#[allow(clippy::module_inception)]
8+
pub mod client;
9+
10+
#[cfg(feature = "http_client")]
11+
pub mod errors;
12+
613
/// Fetch a document, given a `resource` description.
714
pub trait RustReleasesClient {
815
/// The type of error returned by the client implementation.
Lines changed: 53 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
use crate::client::client::Client;
2+
use crate::client::errors::{HttpError, IoError};
13
use crate::{
2-
is_stale, Document, IsStaleError, ResourceFile, RetrievalLocation, RetrievedDocument,
3-
RustReleasesClient,
4+
is_stale, ClientError, Document, IsStaleError, ResourceFile, RetrievalLocation,
5+
RetrievedDocument, RustReleasesClient,
46
};
57
use std::fs;
68
use std::io::{self, BufReader, BufWriter, Read, Write};
@@ -9,61 +11,7 @@ use std::time::Duration;
911

1012
const DEFAULT_MEMORY_SIZE: usize = 4096;
1113

12-
/// A list of errors which may be produced by [`CachedClient::fetch`].
13-
#[derive(Debug, thiserror::Error)]
14-
#[non_exhaustive]
15-
pub enum CachedClientError {
16-
/// Returned if the fetched file was empty.
17-
#[error("Received empty file")]
18-
EmptyFile,
19-
20-
/// Returned if the HTTP client could not fetch an item
21-
#[error(transparent)]
22-
Http(#[from] HttpError),
23-
24-
/// Returned in case of an `std::io::Error`.
25-
#[error(transparent)]
26-
Io(#[from] IoError),
27-
28-
/// Returned in case it wasn't possible to check whether the cache file is
29-
/// stale or not.
30-
#[error(transparent)]
31-
IsStale(#[from] IsStaleError),
32-
}
33-
34-
#[derive(Debug, thiserror::Error)]
35-
pub enum IoError {
36-
#[error("I/O error: {error}{}", format!(" at '{}'", .path.display()))]
37-
Inaccessible { error: io::Error, path: PathBuf },
38-
39-
#[error("I/O error: path at '{path}' is a file, but expected a directory")]
40-
IsFile { path: PathBuf },
41-
42-
#[error("I/O error: {error}")]
43-
Auxiliary { error: io::Error },
44-
}
45-
46-
impl IoError {
47-
pub fn auxiliary(error: io::Error) -> Self {
48-
Self::Auxiliary { error }
49-
}
50-
51-
pub fn inaccessible(error: io::Error, path: PathBuf) -> Self {
52-
Self::Inaccessible { error, path }
53-
}
54-
55-
pub fn is_file(path: PathBuf) -> Self {
56-
Self::IsFile { path }
57-
}
58-
}
59-
60-
/// An error which is returned for a fault which occurred during processing of an HTTP request.
61-
#[derive(Debug, thiserror::Error)]
62-
#[error("HTTP error: {error}")]
63-
pub struct HttpError {
64-
// We box the error since it can be very large.
65-
error: Box<ureq::Error>,
66-
}
14+
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(150);
6715

6816
/// The client to download and cache rust releases.
6917
///
@@ -99,34 +47,34 @@ impl RustReleasesClient for CachedClient {
9947
type Error = CachedClientError;
10048

10149
fn fetch(&self, resource: ResourceFile) -> Result<RetrievedDocument, Self::Error> {
102-
let manifest_path = self.cache_folder.join(resource.name());
103-
let exists = manifest_path.exists();
50+
let path = self.cache_folder.join(resource.name());
51+
let exists = path.exists();
10452

10553
// Returned the cached document if it exists and is not stale
106-
if exists && !is_stale(&manifest_path, self.cache_timeout)? {
107-
let buffer = read_from_path(&manifest_path)?;
54+
if exists && !is_stale(&path, self.cache_timeout)? {
55+
let buffer = read_from_path(&path)?;
10856
let document = Document::new(buffer);
10957

11058
return Ok(RetrievedDocument::new(
11159
document,
112-
RetrievalLocation::Cache(manifest_path),
60+
RetrievalLocation::Cache(path),
11361
));
11462
}
11563

11664
// Ensure we have a place to put the cached document.
11765
if !exists {
118-
setup_cache_folder(&manifest_path)?;
66+
setup_cache_folder(&path)?;
11967
}
12068

121-
let mut reader = fetch_file(resource.url())?;
69+
let client = Client::new(DEFAULT_TIMEOUT);
70+
let mut retrieved = client.fetch(resource).map_err(CachedClientError::from)?;
71+
72+
let document = retrieved.mut_document();
12273

12374
// write to memory
124-
let document = write_document_and_cache(&mut reader, &manifest_path)?;
75+
write_document_and_cache(document, &path)?;
12576

126-
Ok(RetrievedDocument::new(
127-
document,
128-
RetrievalLocation::RemoteUrl(resource.url.to_string()),
129-
))
77+
Ok(retrieved)
13078
}
13179
}
13280

@@ -169,44 +117,49 @@ fn setup_cache_folder(manifest_path: &Path) -> Result<(), CachedClientError> {
169117
Ok(())
170118
}
171119

172-
fn fetch_file(url: &str) -> Result<Box<dyn Read + Send + Sync>, CachedClientError> {
173-
let config = ureq::Agent::config_builder()
174-
.user_agent("rust-releases (github.com/foresterre/rust-releases/issues)")
175-
.proxy(ureq::Proxy::try_from_env())
176-
.build();
177-
178-
let agent = config.new_agent();
179-
180-
let response = agent.get(url).call().map_err(|err| HttpError {
181-
error: Box::new(err),
182-
})?;
183-
184-
let reader = Box::new(response.into_body().into_reader());
185-
186-
Ok(reader)
187-
}
188-
189120
fn write_document_and_cache(
190-
reader: &mut Box<dyn Read + Send + Sync>,
121+
document: &mut Document,
191122
file_path: &Path,
192-
) -> Result<Document, CachedClientError> {
193-
let mut buffer = Vec::with_capacity(DEFAULT_MEMORY_SIZE);
194-
195-
let bytes_read = reader
196-
.read_to_end(&mut buffer)
197-
.map_err(|err| IoError::inaccessible(err, file_path.to_path_buf()))?;
198-
199-
if bytes_read == 0 {
200-
return Err(CachedClientError::EmptyFile);
201-
}
202-
123+
) -> Result<(), CachedClientError> {
203124
let mut file = fs::File::create(file_path)
204125
.map_err(|err| IoError::inaccessible(err, file_path.to_path_buf()))?;
205126

206127
let mut writer = BufWriter::new(&mut file);
207128
writer
208-
.write_all(&buffer)
129+
.write_all(document.buffer())
209130
.map_err(|err| IoError::inaccessible(err, file_path.to_path_buf()))?;
210131

211-
Ok(Document::new(buffer))
132+
Ok(())
133+
}
134+
135+
/// A list of errors which may be produced by [`CachedClient::fetch`].
136+
#[derive(Debug, thiserror::Error)]
137+
#[non_exhaustive]
138+
pub enum CachedClientError {
139+
/// Returned if the fetched file was empty.
140+
#[error("Received empty file")]
141+
EmptyFile,
142+
143+
/// Returned if the HTTP client could not fetch an item
144+
#[error(transparent)]
145+
Http(#[from] HttpError),
146+
147+
/// Returned in case of an `std::io::Error`.
148+
#[error(transparent)]
149+
Io(#[from] IoError),
150+
151+
/// Returned in case it wasn't possible to check whether the cache file is
152+
/// stale or not.
153+
#[error(transparent)]
154+
IsStale(#[from] IsStaleError),
155+
}
156+
157+
impl From<ClientError> for CachedClientError {
158+
fn from(err: ClientError) -> Self {
159+
match err {
160+
ClientError::Empty => CachedClientError::EmptyFile,
161+
ClientError::Http(err) => CachedClientError::Http(err),
162+
ClientError::Io(err) => CachedClientError::Io(err),
163+
}
164+
}
212165
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
use crate::client::errors::{HttpError, IoError};
2+
use crate::{Document, ResourceFile, RetrievalLocation, RetrievedDocument, RustReleasesClient};
3+
use std::io::Read;
4+
use std::time::Duration;
5+
6+
const DEFAULT_MEMORY_SIZE: usize = 4096;
7+
8+
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(150);
9+
10+
/// The client to download and cache rust releases.
11+
///
12+
/// If a cached file is not present, or if a cached file is present, but the copy is outdated,
13+
/// the client will download a new copy of the given resource and store it to the `cache_folder`.
14+
/// If a cached file is present, and the copy is not outdated, the cached file will be returned
15+
/// instead.
16+
pub struct Client {
17+
timeout: Duration,
18+
}
19+
20+
impl Client {
21+
/// Create a new [`Client`].
22+
///
23+
/// ```
24+
/// use std::time::Duration;
25+
/// use rust_releases_io::Client;
26+
/// let timeout = Duration::from_secs(86_400);
27+
///
28+
/// let _client = Client::new(timeout);
29+
/// ```
30+
pub fn new(timeout: Duration) -> Self {
31+
Self { timeout }
32+
}
33+
}
34+
35+
impl Default for Client {
36+
/// Create a new [`Client`].
37+
///
38+
/// ```
39+
/// use rust_releases_io::Client;
40+
///
41+
/// let _client = Client::default();
42+
/// ```
43+
fn default() -> Self {
44+
Self {
45+
timeout: DEFAULT_TIMEOUT,
46+
}
47+
}
48+
}
49+
50+
impl RustReleasesClient for Client {
51+
type Error = ClientError;
52+
53+
fn fetch(&self, resource: ResourceFile) -> Result<RetrievedDocument, Self::Error> {
54+
let mut reader = fetch_file(resource.url(), self.timeout)?;
55+
56+
// write to memory
57+
let document = write_document(&mut reader)?;
58+
59+
Ok(RetrievedDocument::new(
60+
document,
61+
RetrievalLocation::RemoteUrl(resource.url.to_string()),
62+
))
63+
}
64+
}
65+
66+
fn fetch_file(url: &str, timeout: Duration) -> Result<Box<dyn Read + Send + Sync>, ClientError> {
67+
let config = ureq::Agent::config_builder()
68+
.user_agent("rust-releases (github.com/foresterre/rust-releases/issues)")
69+
.proxy(ureq::Proxy::try_from_env())
70+
.timeout_global(Some(timeout))
71+
.build();
72+
73+
let agent = config.new_agent();
74+
75+
let response = agent.get(url).call().map_err(|err| HttpError {
76+
error: Box::new(err),
77+
})?;
78+
79+
let reader = Box::new(response.into_body().into_reader());
80+
81+
Ok(reader)
82+
}
83+
84+
fn write_document(reader: &mut Box<dyn Read + Send + Sync>) -> Result<Document, ClientError> {
85+
let mut buffer = Vec::with_capacity(DEFAULT_MEMORY_SIZE);
86+
87+
let bytes_read = reader
88+
.read_to_end(&mut buffer)
89+
.map_err(IoError::auxiliary)?;
90+
91+
if bytes_read == 0 {
92+
return Err(ClientError::Empty);
93+
}
94+
95+
Ok(Document::new(buffer))
96+
}
97+
98+
/// A list of errors which may be produced by [`Client::fetch`].
99+
#[derive(Debug, thiserror::Error)]
100+
#[non_exhaustive]
101+
pub enum ClientError {
102+
/// Returned if an empty document was fetched.
103+
#[error("Received empty file")]
104+
Empty,
105+
106+
/// Returned if the HTTP client could not fetch an item
107+
#[error(transparent)]
108+
Http(#[from] HttpError),
109+
110+
/// Returned in case of an `std::io::Error`.
111+
#[error(transparent)]
112+
Io(#[from] IoError),
113+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use std::io;
2+
use std::path::PathBuf;
3+
4+
#[derive(Debug, thiserror::Error)]
5+
pub enum IoError {
6+
#[error("I/O error: {error}{}", format!(" at '{}'", .path.display()))]
7+
InaccessiblePath { error: io::Error, path: PathBuf },
8+
9+
#[error("I/O error: path at '{path}' is a file, but expected a directory")]
10+
DirectoryPathIsFile { path: PathBuf },
11+
12+
#[error("I/O error: {error}")]
13+
Auxiliary { error: io::Error },
14+
}
15+
16+
impl IoError {
17+
pub fn auxiliary(error: io::Error) -> Self {
18+
Self::Auxiliary { error }
19+
}
20+
21+
pub fn inaccessible(error: io::Error, path: PathBuf) -> Self {
22+
Self::InaccessiblePath { error, path }
23+
}
24+
25+
pub fn is_file(path: PathBuf) -> Self {
26+
Self::DirectoryPathIsFile { path }
27+
}
28+
}
29+
30+
/// An error which is returned for a fault which occurred during processing of an HTTP request.
31+
#[derive(Debug, thiserror::Error)]
32+
#[error("HTTP error: {error}")]
33+
pub struct HttpError {
34+
// We box the error since it can be very large.
35+
pub(crate) error: Box<ureq::Error>,
36+
}

crates/rust-releases-io/src/document.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ impl RetrievedDocument {
2525
pub fn retrieval_location(&self) -> &RetrievalLocation {
2626
&self.retrieval_location
2727
}
28+
29+
/// Exclusive access to the document
30+
pub fn mut_document(&mut self) -> &mut Document {
31+
&mut self.document
32+
}
2833
}
2934

3035
/// A `Document` represents a resource which can be used as an input to construct a `ReleaseIndex`.

crates/rust-releases-io/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ pub use crate::{
1616
};
1717

1818
#[cfg(feature = "http_client")]
19-
pub use crate::client::cached_client::{CachedClient, CachedClientError};
19+
pub use crate::client::{
20+
cached_client::CachedClient, cached_client::CachedClientError, client::Client,
21+
client::ClientError,
22+
};
2023

2124
/// A macro used to feature gate tests which fetch resources from third party services.
2225
///

0 commit comments

Comments
 (0)