Skip to content
Open
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
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ We are using [prek](https://prek.j178.dev/) (`pre-commit` Rust alternative) hook

Pre-commit still works for this project because `prek` and `pre-commit` share the same configuration file. However, the project may switch to a `prek`-specific configuration in the future.

Error messages should begin with a lowercase letter and should not end with
punctuation unless they contain multiple sentences. This follows the convention
used by Rust's standard library and keeps chained errors readable.

### Tests that use database, cache, or other external resources

Some tests use a database, cache, or other external resources. All these tests
Expand Down
14 changes: 7 additions & 7 deletions cot-cli/src/migration_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ use crate::utils::{CargoTomlManager, PackageManager};

pub fn make_migrations(path: &Path, options: MigrationGeneratorOptions) -> anyhow::Result<()> {
let Some(manager) = CargoTomlManager::from_path(path)? else {
bail!("Cargo.toml not found in the specified directory or any parent directory.")
bail!("no Cargo.toml found in the specified directory or any parent directory")
};

match manager {
CargoTomlManager::Workspace(workspace) => {
let Some(package) = workspace.get_current_package_manager() else {
bail!(
"Generating migrations for workspaces is not supported yet. \
Please generate migrations for each package separately."
"generating migrations for workspaces is not supported yet. \
Please generate migrations for each package separately"
);
};
make_package_migrations(package, options)
Expand Down Expand Up @@ -75,15 +75,15 @@ pub fn create_new_migration(
options: MigrationGeneratorOptions,
) -> anyhow::Result<()> {
let Some(manager) = CargoTomlManager::from_path(path)? else {
bail!("Cargo.toml not found in the specified directory or any parent directory.")
bail!("no Cargo.toml found in the specified directory or any parent directory")
};

match manager {
CargoTomlManager::Workspace(workspace) => {
let Some(package) = workspace.get_current_package_manager() else {
bail!(
"Generating migrations for workspaces is not supported yet. \
Please generate migrations for each package separately."
"generating migrations for workspaces is not supported yet. \
Please generate migrations for each package separately"
);
};
create_package_new_migration(package, name, options)
Expand Down Expand Up @@ -137,7 +137,7 @@ pub fn list_migrations(path: &Path) -> anyhow::Result<HashMap<String, Vec<String
}
Ok(migration_list)
} else {
bail!("Cargo.toml not found in the specified directory or any parent directory.")
bail!("no Cargo.toml found in the specified directory or any parent directory")
}
}

Expand Down
2 changes: 1 addition & 1 deletion cot-cli/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ impl CargoTomlManager {
}

(None, None) => {
bail!("Cargo.toml is not a valid workspace or package manifest");
bail!("invalid Cargo.toml: expected a workspace or package manifest");
}
};

Expand Down
12 changes: 6 additions & 6 deletions cot-core/src/error/error_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ impl Error {
/// ```
/// use cot::Error;
///
/// let error = Error::internal("An error occurred");
/// let error = Error::internal("an error occurred");
/// let error = Error::internal(std::io::Error::new(
/// std::io::ErrorKind::Other,
/// "An error occurred",
/// "an error occurred",
/// ));
/// ```
#[must_use]
Expand All @@ -79,10 +79,10 @@ impl Error {
/// use cot::{Error, StatusCode};
///
/// // Create a 400 Bad Request error
/// let error = Error::with_status("Invalid input", StatusCode::BAD_REQUEST);
/// let error = Error::with_status("invalid input", StatusCode::BAD_REQUEST);
///
/// // Create a 403 Forbidden error
/// let error = Error::with_status("Access denied", StatusCode::FORBIDDEN);
/// let error = Error::with_status("access denied", StatusCode::FORBIDDEN);
/// ```
#[must_use]
pub fn with_status<E>(error: E, status_code: StatusCode) -> Self
Expand All @@ -109,10 +109,10 @@ impl Error {
/// ```
/// use cot::{Error, StatusCode};
///
/// let error = Error::internal("Something went wrong");
/// let error = Error::internal("something went wrong");
/// assert_eq!(error.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
///
/// let error = Error::with_status("Bad request", StatusCode::BAD_REQUEST);
/// let error = Error::with_status("bad request", StatusCode::BAD_REQUEST);
/// assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
/// ```
#[must_use]
Expand Down
2 changes: 1 addition & 1 deletion cot-core/src/request/extractors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ impl<D: DeserializeOwned> FromRequest for Json<D> {

#[cfg(feature = "json")]
#[derive(Debug, thiserror::Error)]
#[error("JSON deserialization error: {0}")]
#[error("failed to deserialize JSON: {0}")]
struct JsonDeserializeError(serde_path_to_error::Error<serde_json::Error>);
#[cfg(feature = "json")]
impl_into_cot_error!(JsonDeserializeError, BAD_REQUEST);
Expand Down
2 changes: 1 addition & 1 deletion cot-core/src/response/into_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ impl<D: serde::Serialize> IntoResponse for crate::json::Json<D> {

#[cfg(feature = "json")]
#[derive(Debug, thiserror::Error)]
#[error("JSON serialization error: {0}")]
#[error("failed to serialize JSON: {0}")]
struct JsonSerializeError(serde_path_to_error::Error<serde_json::Error>);
#[cfg(feature = "json")]
impl_into_cot_error!(JsonSerializeError, INTERNAL_SERVER_ERROR);
Expand Down
2 changes: 1 addition & 1 deletion cot/src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ async fn login(
let mut context = LoginForm::build_context(&mut request).await?;
context.add_error(
FormErrorTarget::Form,
FormFieldValidationError::from_static("Invalid username or password"),
FormFieldValidationError::from_static("invalid username or password"),
);
context
}
Expand Down
2 changes: 1 addition & 1 deletion cot/src/db/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub enum MigrationEngineError {
#[error("error running migration: {0}")]
Custom(String),
/// An I/O error occurred while writing output (e.g. during dry-run).
#[error("I/O error while writing migration output: {0}")]
#[error("failed to write migration output: {0}")]
Io(#[from] io::Error),
}

Expand Down
4 changes: 2 additions & 2 deletions cot/src/email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ pub enum EmailMessageError {
#[error("{ERROR_PREFIX} failed to build email message: {0}")]
BuildError(Box<dyn StdError + Send + Sync + 'static>),
/// A required field is missing in the email message.
#[error("{ERROR_PREFIX} The `{0}` field is required but was not set")]
#[error("{ERROR_PREFIX} the `{0}` field is required but was not set")]
MissingField(String),
}

Expand Down Expand Up @@ -370,7 +370,7 @@ mod tests {
let err = res.err().unwrap();
assert_eq!(
err.to_string(),
"email message build error: The `from` field is required but was not set"
"email message build error: the `from` field is required but was not set"
);
}

Expand Down
2 changes: 1 addition & 1 deletion cot/src/error/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ impl FromRequestHead for RequestOuterError {
let error = head.extensions.get::<RequestOuterError>();
error
.ok_or_else(|| {
Error::internal("No error found in request head. Make sure you use this extractor in an error handler.")
Error::internal("no error found in request head. Make sure you use this extractor in an error handler.")
}).cloned()
}
}
Expand Down
29 changes: 22 additions & 7 deletions cot/src/error/not_found.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use thiserror::Error;
/// let error = NotFound::new();
///
/// // Create a 404 error with a custom message
/// let error = NotFound::with_message("User not found");
/// let error = NotFound::with_message("user not found");
/// ```
///
/// ["404 Not Found"]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/404
Expand All @@ -33,7 +33,7 @@ pub struct NotFound {
impl_into_cot_error!(NotFound, NOT_FOUND);

impl NotFound {
/// Creates a new `NotFound` error with a generic "Not Found" message.
/// Creates a new `NotFound` error with a generic "not found" message.
///
/// This is the most common way to create a 404 error when you don't need
/// to provide additional context about what was not found.
Expand Down Expand Up @@ -61,9 +61,9 @@ impl NotFound {
/// ```
/// use cot::error::NotFound;
///
/// let error = NotFound::with_message("User with ID 123 not found");
/// let error = NotFound::with_message("user with ID 123 not found");
/// let page_name = "home";
/// let error = NotFound::with_message(format!("Page '{}' not found", page_name));
/// let error = NotFound::with_message(format!("page '{}' not found", page_name));
/// ```
#[must_use]
pub fn with_message<T: Into<String>>(message: T) -> Self {
Expand Down Expand Up @@ -99,22 +99,37 @@ pub enum Kind {
///
/// This variant is used when the router cannot find a route that matches
/// the request's path and method.
#[error("Not Found")]
#[error("not found")]
#[non_exhaustive]
FromRouter,
/// A generic 404 error without additional context.
///
/// This variant is used for basic "not found" errors where no specific
/// message or context is needed.
#[error("Not Found")]
#[error("not found")]
#[non_exhaustive]
Custom,
/// A 404 error with a custom message providing additional context.
///
/// This variant includes a custom message that describes what specifically
/// was not found, which can be useful for debugging or providing more
/// informative error responses.
#[error("Not Found: {0}")]
#[error("not found: {0}")]
#[non_exhaustive]
WithMessage(String),
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn messages_are_idiomatic() {
assert_eq!(NotFound::new().to_string(), "not found");
assert_eq!(
NotFound::with_message("resource unavailable").to_string(),
"not found: resource unavailable"
);
assert_eq!(NotFound::router().to_string(), "not found");
}
}
39 changes: 29 additions & 10 deletions cot/src/form.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,57 +130,57 @@ impl<T: Form> FormResult<T> {
#[error("{message}")]
pub enum FormFieldValidationError {
/// The field is required.
#[error("This field is required.")]
#[error("this field is required")]
Required,
/// The field value is too long.
#[error("This exceeds the maximum length of {max_length}.")]
#[error("this exceeds the maximum length of {max_length}")]
MaximumLengthExceeded {
/// The maximum length of the field.
max_length: u32,
},

/// The field value is too short.
#[error("This is below the minimum length of {min_length}.")]
#[error("this is below the minimum length of {min_length}")]
MinimumLengthNotMet {
/// The minimum length of the field.
min_length: u32,
},

/// The field value is below the permitted minimum.
#[error("This is below the minimum value of {min_value}.")]
#[error("this is below the minimum value of {min_value}")]
MinimumValueNotMet {
/// The minimum permitted value.
min_value: String,
},

/// The field value exceeds the permitted maximum.
#[error("This exceeds the maximum value of {max_value}.")]
#[error("this exceeds the maximum value of {max_value}")]
MaximumValueExceeded {
/// The maximum permitted value.
max_value: String,
},
/// The field value is an ambiguous datetime.
#[error("The datetime value `{datetime}` is ambiguous.")]
#[error("the datetime value `{datetime}` is ambiguous")]
AmbiguousDateTime {
/// The ambiguous datetime value.
datetime: NaiveDateTime,
},
/// The field value is a non-existent local datetime.
#[error("Local datetime {datetime} does not exist for the specified timezone {timezone}.")]
#[error("local datetime {datetime} does not exist for the specified timezone {timezone}")]
NonExistentLocalDateTime {
/// The non-existent local datetime value.
datetime: NaiveDateTime,
/// The timezone in which the datetime was specified.
timezone: Tz,
},
/// The field value is required to be true.
#[error("This field must be checked.")]
#[error("this field must be checked")]
BooleanRequiredToBeTrue,
/// The field value is invalid.
#[error("Value is not valid for this field.")]
#[error("value is not valid for this field")]
InvalidValue(String),
/// An error occurred while getting the field value.
#[error("Error getting field value: {0}")]
#[error("error getting field value: {0}")]
FormFieldValueError(#[from] FormFieldValueError),
/// Custom error with a given message.
#[error("{0}")]
Expand Down Expand Up @@ -868,4 +868,23 @@ mod tests {
panic!("Expected RequestError");
}
}
#[test]
fn built_in_validation_errors_have_idiomatic_messages() {
assert_eq!(
FormFieldValidationError::Required.to_string(),
"this field is required"
);
assert_eq!(
FormFieldValidationError::maximum_length_exceeded(10).to_string(),
"this exceeds the maximum length of 10"
);
assert_eq!(
FormFieldValidationError::minimum_length_not_met(2).to_string(),
"this is below the minimum length of 2"
);
assert_eq!(
FormFieldValidationError::invalid_value("invalid").to_string(),
"value is not valid for this field"
);
}
}
4 changes: 2 additions & 2 deletions cot/src/form/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1066,7 +1066,7 @@ macro_rules! impl_float_as_form_field {

if parsed.is_nan() || parsed.is_infinite() {
return Err(FormFieldValidationError::from_static(
"Cannot have NaN or inf as form input values",
"cannot have NaN or inf as form input values",
));
}

Expand Down Expand Up @@ -1801,7 +1801,7 @@ mod tests {
assert_eq!(
value,
Err(FormFieldValidationError::from_static(
"Cannot have NaN or inf as form input values"
"cannot have NaN or inf as form input values"
))
);
}
Expand Down
2 changes: 1 addition & 1 deletion cot/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1670,7 +1670,7 @@ async fn set_current_db(conn: &mut Connection, db_num: usize) {
enum RedisDbAllocatorError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("Redis error: {0}")]
#[error("redis error: {0}")]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Redis is a project name, so we should keep it as is.

Redis(String),
}

Expand Down
6 changes: 3 additions & 3 deletions docs/error-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,15 @@ async fn view(request: Request) -> cot::Result<Response> {

// 404 with custom message
return Err(NotFound::with_message(
"The article you're looking for doesn't exist".to_string()
"the article you're looking for doesn't exist".to_string()
))?;

// 500 Internal Server Error
return Err(Error::internal("Something went wrong"));
return Err(Error::internal("something went wrong"));
// or, by re-raising a custom error:
return Err(Error::internal(std::io::Error::other("oh no!")));
// or, by panicking:
panic!("Something went wrong");
panic!("something went wrong");
}
```

Expand Down
2 changes: 1 addition & 1 deletion docs/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ async fn handle_form(mut request: Request) -> cot::Result<Response> {
let mut context = ArticleForm::build_context(&mut request).await?;
context.add_error(
FormErrorTarget::Field("title"),
FormFieldValidationError::from_static("Title contains spam")
FormFieldValidationError::from_static("title contains spam")
);

// Re-render form with error
Expand Down
Loading