Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove ModelError in favour of strongly-typed errors #327

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
**Breaking changes**:
- ([#325](https://github.com/ramsayleung/rspotify/pull/325)) The `auth_code`, `auth_code_pkce`, `client_creds`, `clients::base` and `clients::oauth` modules have been removed from the public API; you should access the same types from their parent modules instead
- ([#326](https://github.com/ramsayleung/rspotify/pull/326)) The `rspotify::clients::mutex` module has been renamed to `rspotify::sync`
- ([#327](https://github.com/ramsayleung/rspotify/pull/327)) `ModelError` has been replaced with the more precise error types `ReadTokenCacheError` and `WriteTokenCacheError`
- ([#330](https://github.com/ramsayleung/rspotify/pull/330)) `search` now accepts `Option<IncludeExternal>` instead of `Option<&IncludeExternal>`
- ([#330](https://github.com/ramsayleung/rspotify/pull/330)) `featured_playlists` now accepts `Option<chrono::DateTime<chrono::Utc>>` instead of `Option<&chrono::DateTime<chrono::Utc>>`
- ([#330](https://github.com/ramsayleung/rspotify/pull/330)) `current_user_top_artists[_manual]` and `current_user_top_tracks[_manual]` now accept `Option<TimeRange>` instead of `Option<&TimeRange>`
Expand Down
133 changes: 117 additions & 16 deletions rspotify-model/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
//! All objects related to the auth flows defined by Spotify API

use crate::{
custom_serde::{duration_second, space_separated_scopes},
ModelResult,
};
use crate::custom_serde::{duration_second, space_separated_scopes};

use std::{
collections::{HashMap, HashSet},
fs,
io::{Read, Write},
fs, io,
path::Path,
};

use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use thiserror::Error;

/// Spotify access token information
///
Expand Down Expand Up @@ -56,22 +53,23 @@ impl Default for Token {

impl Token {
/// Tries to initialize the token from a cache file.
pub fn from_cache<T: AsRef<Path>>(path: T) -> ModelResult<Self> {
let mut file = fs::File::open(path)?;
let mut tok_str = String::new();
file.read_to_string(&mut tok_str)?;
let tok = serde_json::from_str::<Token>(&tok_str)?;
pub fn from_cache<T: AsRef<Path>>(path: T) -> Result<Self, ReadTokenCacheError> {
let json = read_file(path.as_ref()).map_err(ReadTokenCacheError::Reading)?;

let tok =
serde_json::from_slice::<Token>(&*json).map_err(ReadTokenCacheError::Deserializing)?;

Ok(tok)
}

/// Saves the token information into its cache file.
pub fn write_cache<T: AsRef<Path>>(&self, path: T) -> ModelResult<()> {
let token_info = serde_json::to_string(&self)?;
pub fn write_cache<T: AsRef<Path>>(&self, path: T) -> Result<(), WriteTokenCacheError> {
// `serde_json::to_vec` only errors if our `Serialize` implementation
// fails, which it shouldn’t, or we try to serialize a map with
// non-string keys, which we don’t.
let json = serde_json::to_vec(&self).unwrap();

let mut file = fs::OpenOptions::new().write(true).create(true).open(path)?;
file.set_len(0)?;
file.write_all(token_info.as_bytes())?;
write_file(path.as_ref(), &*json).map_err(|inner| WriteTokenCacheError { inner })?;

Ok(())
}
Expand All @@ -95,6 +93,109 @@ impl Token {
}
}

/// An error reading a cached [`Token`].
#[derive(Debug, Error)]
#[error("failed to read token from cache")]
pub enum ReadTokenCacheError {
/// There was an error reading the cache file into memory.
Reading(#[source] ReadFileError),

/// There was an error deserializing the contents of the cache file.
Deserializing(#[source] serde_json::Error),
}

/// An error writing a [`Token`] to its cache.
#[derive(Debug, Error)]
#[non_exhaustive]
Copy link
Collaborator

@marioortizmanero marioortizmanero Jun 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this use non_exhaustive, for future-proofing? And why not a tuple struct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could’ve been a tuple struct I suppose — should I change it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say we don't really need to future-proof anything here? Not sure what else we would need to add into the error.

#[error("failed to write token to cache")]
pub struct WriteTokenCacheError {
/// The underlying error in writing the [`Token`] file cache.
#[source]
pub inner: WriteFileError,
}

fn read_file(path: &Path) -> Result<Vec<u8>, ReadFileError> {
fs::read(path).map_err(|inner| ReadFileError {
inner,
path: Box::from(path),
})
}

/// An error reading a file.
#[derive(Debug, Error)]
#[error("failed to read file {}", path.display())]
pub struct ReadFileError {
// Intentionally not exposed to allow future API evolution, e.g., moving
// this to a enum variants `Open(io::Error)` and `Read(io::Error)`
#[source]
inner: io::Error,
path: Box<Path>,
}

impl ReadFileError {
/// Returns a shared reference to the underlying I/O that caused this.
#[must_use]
pub fn io(&self) -> &io::Error {
&self.inner
}

/// Consumes this error type, returning the underlying inner I/O error.
///
/// It is not recommended to use this method just to unify error types, as
/// you will lose valuable information in the error message.
#[must_use]
pub fn into_io(self) -> io::Error {
self.inner
}

/// Returns a shared reference to the path that could not be read.
#[must_use]
pub fn path(&self) -> &Path {
&*self.path
}
}

fn write_file(path: &Path, bytes: &[u8]) -> Result<(), WriteFileError> {
fs::write(path, bytes).map_err(|inner| WriteFileError {
inner,
path: Box::from(path),
})
}

/// An error writing a file.
#[derive(Debug, Error)]
#[error("failed to write file {}", path.display())]
pub struct WriteFileError {
// Intentionally not exposed to allow future API evolution, e.g., moving
// this to an enum variant `Open(io::Error)` and `Write(io::Error)`
#[source]
inner: io::Error,
path: Box<Path>,
}

impl WriteFileError {
/// Returns a shared reference to the underlying I/O that caused this.
#[must_use]
pub fn io(&self) -> &io::Error {
&self.inner
}

/// Consumes this error type, returning the underlying inner I/O error.
///
/// It is not recommended to use this method just to unify error types, as
/// you will lose valuable information in the error message.
#[must_use]
pub fn into_io(self) -> io::Error {
self.inner
}

/// Returns a shared reference to the path that could not be written to.
#[must_use]
pub fn path(&self) -> &Path {
&*self.path
}
}

#[cfg(test)]
mod test {
use crate::Token;
Expand Down
11 changes: 0 additions & 11 deletions rspotify-model/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use serde::Deserialize;
use thiserror::Error;

pub type ApiResult<T> = Result<T, ApiError>;
pub type ModelResult<T> = Result<T, ModelError>;

/// Matches errors that are returned from the Spotfiy
/// API as part of the JSON response object.
Expand All @@ -22,13 +21,3 @@ pub enum ApiError {
reason: String,
},
}

/// Groups up the kinds of errors that may happen in this crate.
#[derive(Debug, Error)]
pub enum ModelError {
#[error("json parse error: {0}")]
ParseJson(#[from] serde_json::Error),

#[error("input/output error: {0}")]
Io(#[from] std::io::Error),
}
3 changes: 2 additions & 1 deletion src/client_creds.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{
clients::BaseClient,
http::{Form, HttpClient},
model::ReadTokenCacheError,
params,
sync::Mutex,
ClientResult, Config, Credentials, Token,
Expand Down Expand Up @@ -95,7 +96,7 @@ impl ClientCredsSpotify {
/// * The read token is expired
/// * The cached token is disabled in the config
#[maybe_async]
pub async fn read_token_cache(&self) -> ClientResult<Option<Token>> {
pub async fn read_token_cache(&self) -> Result<Option<Token>, ReadTokenCacheError> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change can be avoided by making this error part of ClientError

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But then all code that deals in ClientErrors would now have one more variant to think about even though it can’t actually be created from most code, and similarly any code that calls this function now has to deal with every variant of ClientError even though only one actually can be created.

I suppose it would technically fit ClientError’s stated purpose to use it here, but the API can be made more strongly typed by not using it I think.

Copy link
Collaborator

@marioortizmanero marioortizmanero Jun 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. My reasoning was that if ReadTokenCacheError isn't in ClientError, then if someone used to return a ClientError, they will now have to define their own custom error type for both. But I guess that's not really a huge issue, as most libraries will have their own error anyway?

if !self.get_config().token_cached {
log::info!("Token cache read ignored (not configured)");
return Ok(None);
Expand Down
5 changes: 4 additions & 1 deletion src/clients/oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ pub trait OAuthClient: BaseClient {
/// the application re-authenticate.
///
/// [`ClientCredsSpotify::read_token_cache`]: crate::client_creds::ClientCredsSpotify::read_token_cache
async fn read_token_cache(&self, allow_expired: bool) -> ClientResult<Option<Token>> {
async fn read_token_cache(
&self,
allow_expired: bool,
) -> Result<Option<Token>, ReadTokenCacheError> {
marioortizmanero marked this conversation as resolved.
Show resolved Hide resolved
if !self.get_config().token_cached {
log::info!("Auth token cache read ignored (not configured)");
return Ok(None);
Expand Down
7 changes: 2 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,8 @@ pub enum ClientError {
#[error("cli error: {0}")]
Cli(String),

#[error("cache file error: {0}")]
CacheFile(String),

#[error("model error: {0}")]
Model(#[from] model::ModelError),
#[error("error updating token cache: {0}")]
UpdateTokenCache(#[from] model::WriteTokenCacheError),
}

// The conversion has to be done manually because it's in a `Box<T>`
Expand Down