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
11 changes: 10 additions & 1 deletion blueprint/config/src/lib.rs.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,13 @@ pub struct DatabaseConfig {
/// you can set that location using the `APP_DOTENV_CONFIG_DIR` environment variable.
/// This is useful when they are mounted at separate locations in a Docker container, for example.
///
/// Configuration settings are loaded from these sources (in that order so that latter soruces override former):
/// Configuration settings are loaded from these sources (in that order so that latter sources override the former):
/// * the `config/app.toml` file
/// * the `config/environments/<development|production|test>.toml` files depending on the environment
/// * environment variables
///
/// # Errors
/// Returns an error if any of the configuration sources could not be read.
pub fn load_config<'a, T>(env: &Environment) -> Result<T, anyhow::Error>
where
T: Deserialize<'a>,
Expand Down Expand Up @@ -186,6 +189,9 @@ impl Display for Environment {
/// Returns the currently active environment.
///
/// If the `APP_ENVIRONMENT` env var is set, the application environment is parsed from that (which might fail if an invalid environment is set). If the env var is not set, [`Environment::Development`] is returned.
///
/// # Errors
/// Returns an error if the `APP_ENVIRONMENT` env var is set to an invalid environment.
pub fn get_env() -> Result<Environment, anyhow::Error> {
if let Ok(val) = env::var("APP_ENVIRONMENT") {
info!(r#"Setting environment from APP_ENVIRONMENT: "{}""#, val);
Expand All @@ -199,6 +205,9 @@ pub fn get_env() -> Result<Environment, anyhow::Error> {
/// Parses an [`Environment`] from a string.
///
/// The environment can be passed in different forms, e.g. "dev", "development", "prod", etc. If an invalid environment is passed, an error is returned.
///
/// # Errors
/// Returns an error if the environment is not one of `dev`, `development`, `prod`, `production` or `test`.
pub fn parse_env(env: &str) -> Result<Environment, anyhow::Error> {
let env = &env.to_lowercase();
match env.as_str() {
Expand Down
20 changes: 15 additions & 5 deletions blueprint/db/src/entities/tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub struct Task {

/// A changeset representing the data that is intended to be used to either create a new task or update an existing task.
///
/// Changesets are validatated in the [`create`] and [`update`] functions which return an [Result::Err] if validation fails.
/// Changesets are validated in the [`create`] and [`update`] functions which return an [`Result::Err`] if validation fails.
///
/// Changesets can also be used to generate fake data for tests when the `test-helpers` feature is enabled:
///
Expand All @@ -34,6 +34,8 @@ pub struct TaskChangeset {
}

/// Load all [`Task`]s from the database.
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
pub async fn load_all(
executor: impl sqlx::Executor<'_, Database = Postgres>,
) -> Result<Vec<Task>, crate::Error> {
Expand All @@ -45,7 +47,9 @@ pub async fn load_all(

/// Load one [`Task`] from the database identified by its ID.
///
/// If no record can be found for the ID, a [`crate::Error::NoRecordFound`] will be returned.
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
/// - If no record can be found for the ID, a [`crate::Error::NoRecordFound`] will be returned.
pub async fn load(
id: Uuid,
executor: impl sqlx::Executor<'_, Database = Postgres>,
Expand All @@ -59,7 +63,9 @@ pub async fn load(

/// Delete a [`Task`] from the database identified by its ID.
///
/// If no record can be found for the ID, a [`crate::Error::NoRecordFound`] will be returned.
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
/// - If no record can be found for the ID, a [`crate::Error::NoRecordFound`] will be returned.
pub async fn delete(
id: Uuid,
executor: impl sqlx::Executor<'_, Database = Postgres>,
Expand All @@ -75,7 +81,9 @@ pub async fn delete(

/// Create a task in the database with the data in the passed [`TaskChangeset`].
///
/// If the data in the changeset isn't valid, a [`crate::Error::ValidationError`] will be returned, otherwise the created task is returned.
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
/// - If the data in the changeset isn't valid, a [`crate::Error::ValidationError`] will be returned, otherwise the created task is returned.
pub async fn create(
task: TaskChangeset,
executor: impl sqlx::Executor<'_, Database = Postgres>,
Expand All @@ -98,7 +106,9 @@ pub async fn create(

/// Updates a task in the database with the data in the passed [`TaskChangeset`].
///
/// If the data in the changeset isn't valid, a [`crate::Error::ValidationError`] will be returned, otherwise the updated [`Task`] is returned. If no record can be found for the ID, a [`crate::Error::NoRecordFound`] will be returned.
/// # Errors
/// - If there is a generic database error, a [`crate::Error::DbError`] will be returned.
/// - If the data in the changeset isn't valid, a [`crate::Error::ValidationError`] will be returned, otherwise the updated [`Task`] is returned. If no record can be found for the ID, a [`crate::Error::NoRecordFound`] will be returned.
pub async fn update(
id: Uuid,
task: TaskChangeset,
Expand Down
5 changes: 4 additions & 1 deletion blueprint/db/src/entities/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ pub struct User {
/// Loads a user based on the passed token.
///
/// If no user exists for the token, [`Option::None`] is returned, otherwise `Option::Some(User)` is returned.
///
/// # Errors
/// Returns [`crate::Error::DbError`] if a general database error occurs.
pub async fn load_with_token(
token: &str,
executor: impl sqlx::Executor<'_, Database = Postgres>,
) -> Result<Option<User>, anyhow::Error> {
) -> Result<Option<User>, crate::Error> {
Ok(
sqlx::query_as!(User, "SELECT id, name FROM users WHERE token = $1", token)
.fetch_optional(executor)
Expand Down
6 changes: 6 additions & 0 deletions blueprint/db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ pub mod entities;
/// ```
///
/// Transactions are rolled back automatically when they are dropped without having been committed.
///
/// # Errors
/// Returns a [`sqlx_core::error`] when the transaction is unsuccessful.
pub async fn transaction(
db_pool: &DbPool,
) -> Result<Transaction<'static, Postgres>, anyhow::Error> {
Expand Down Expand Up @@ -54,6 +57,9 @@ pub enum Error {
}

/// Creates a connection pool to the database specified in the passed [`{{project-name}}-config::DatabaseConfig`]
///
/// # Errors
/// Returns a [`sqlx_core::error`] when the transaction is unsuccessful.
pub async fn connect_pool(config: DatabaseConfig) -> Result<DbPool, anyhow::Error> {
let pool = PgPoolOptions::new()
.connect(config.url.as_str())
Expand Down
6 changes: 6 additions & 0 deletions blueprint/db/src/test_helpers/mod.rs.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ pub mod users;
///
/// This sets up a dedicated database as a fork of the main test database as configured in `.env.test`. The database can be used in a test case to ensure the test case is isolated from other test cases. The function returns a connection pool connected to the created database.
/// This function is automatically called by the [`{{project-name}}-macros::db_test`] macro. The return connection pool is passed to the test case via the [`{{project-name}}-macros::DbTestContext`].
///
/// # Panics
/// This function panics if it cannot connect to the database.
#[allow(unused)]
pub async fn setup_db(config: &DatabaseConfig) -> DbPool {
let test_db_config = prepare_db(config).await;
Expand All @@ -28,6 +31,9 @@ pub async fn setup_db(config: &DatabaseConfig) -> DbPool {
/// Drops a dedicated database for a test case.
///
/// This function is automatically called by the [`{{project-name}}-macros::db_test`] macro. It ensures test-specific database are cleaned up after each test run so we don't end up with large numbers of unused databases.
///
/// # Panics
/// This function panics if connection to the database fails, the database name cannot be retrieved, or the database cannot be dropped.
pub async fn teardown_db(db_pool: DbPool) {
let mut connect_options = db_pool.connect_options();
let db_config = Arc::make_mut(&mut connect_options);
Expand Down
3 changes: 2 additions & 1 deletion blueprint/db/src/test_helpers/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use sqlx::postgres::PgPool;

/// A changeset representing the data that is intended to be used to either create a new user or update an existing user.
///
/// Changesets are validated in the [`create`] function which return an [Result::Err] if validation fails.
/// Changesets are validated in the [`create`] function that returns an [`Result::Err`] if validation fails.
///
/// Changesets can also be used to generate fake data for tests when the `test-helpers` feature is enabled:
///
Expand All @@ -23,6 +23,7 @@ pub struct UserChangeset {

/// Creates a user in the database with the data in the passed [`UserChangeset`].
///
/// # Errors
/// If the data in the changeset isn't valid, a [`crate::Error::ValidationError`] will be returned, otherwise the created user is returned.
pub async fn create(user: UserChangeset, db: &PgPool) -> Result<User, anyhow::Error> {
let record = sqlx::query!(
Expand Down
31 changes: 26 additions & 5 deletions blueprint/web/src/controllers/tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ use uuid::Uuid;

/// Creates a task in the database.
///
/// This function creates a task in the database (see [`{{crate_name}}_db::entities::tasks::create`]) based on a [`{{crate_name}}_db::entities::tasks::TaskChangeset`] (sent as JSON). If the task is created successfully, a 201 response is returned with the created [`{{crate_name}}_db::entities::tasks::Task`]'s JSON representation in the response body. If the changeset is invalid, a 422 response is returned.
/// This function creates a task in the database (see [`{{crate_name}}_db::entities::tasks::create`]) based on a [`{{crate_name}}_db::entities::tasks::TaskChangeset`] (sent as JSON). If the task is created successfully, a 201 response is returned with the created [`{{crate_name}}_db::entities::tasks::Task`]'s JSON representation in the response body.
///
/// # Errors
/// - 422 Unprocessable Entity: Invalid changeset
#[axum::debug_handler]
pub async fn create(
State(app_state): State<SharedAppState>,
Expand All @@ -19,9 +22,12 @@ pub async fn create(

/// Creates multiple tasks in the database.
///
/// This function creates multiple tasks in the database (see [`{{crate_name}}_db::entities::tasks::create`]) based on [`{{crate_name}}_db::entities::tasks::TaskChangeset`]s (sent as JSON). If all tasks are created successfully, a 201 response is returned with the created [`{{crate_name}}_db::entities::tasks::Task`]s' JSON representation in the response body. If any of the passed changesets is invalid, a 422 response is returned.
/// This function creates multiple tasks in the database (see [`{{crate_name}}_db::entities::tasks::create`]) based on [`{{crate_name}}_db::entities::tasks::TaskChangeset`]s (sent as JSON). If all tasks are created successfully, a 201 response is returned with the created [`{{crate_name}}_db::entities::tasks::Task`]s' JSON representation in the response body.
///
/// This function creates all tasks in a transaction so that either all are created successfully or none is.
///
/// # Errors
/// - 422 Unprocessable Entity: Any changeset is invalid
#[axum::debug_handler]
pub async fn create_batch(
State(app_state): State<SharedAppState>,
Expand All @@ -43,6 +49,9 @@ pub async fn create_batch(
/// Reads and responds with all the tasks currently present in the database.
///
/// This function reads all [`{{crate_name}}_db::entities::tasks::Task`]s from the database (see [`{{crate_name}}_db::entities::tasks::load_all`]) and responds with their JSON representations.
///
/// # Errors
/// - 500 Internal Server Error: Database connection failed or other generic database error
#[axum::debug_handler]
pub async fn read_all(State(app_state): State<SharedAppState>) -> Result<Json<Vec<tasks::Task>>, Error> {
let tasks = tasks::load_all(&app_state.db_pool).await?;
Expand All @@ -54,7 +63,11 @@ pub async fn read_all(State(app_state): State<SharedAppState>) -> Result<Json<Ve

/// Reads and responds with a task identified by its ID.
///
/// This function reads one [`{{crate_name}}_db::entities::tasks::Task`] identified by its ID from the database (see [`{{crate_name}}_db::entities::tasks::load`]) and responds with its JSON representations. If no task is found for the ID, a 404 response is returned.
/// This function reads one [`{{crate_name}}_db::entities::tasks::Task`] identified by its ID from the database (see [`{{crate_name}}_db::entities::tasks::load`]) and responds with its JSON representations.
///
/// # Errors
/// - 404 Not found: No task with the given ID was found.
/// - 500 Internal Server Error: Database connection failed or other generic database error
#[axum::debug_handler]
pub async fn read_one(
State(app_state): State<SharedAppState>,
Expand All @@ -66,7 +79,11 @@ pub async fn read_one(

/// Updates a task in the database.
///
/// This function updates a task identified by its ID in the database (see [`{{crate_name}}_db::entities::tasks::update`]) with the data from the passed [`{{crate_name}}_db::entities::tasks::TaskChangeset`] (sent as JSON). If the task is updated successfully, a 200 response is returned with the created [`{{crate_name}}_db::entities::tasks::Task`]'s JSON representation in the response body. If the changeset is invalid, a 422 response is returned.
/// This function updates a task identified by its ID in the database (see [`{{crate_name}}_db::entities::tasks::update`]) with the data from the passed [`{{crate_name}}_db::entities::tasks::TaskChangeset`] (sent as JSON). If the task is updated successfully, a 200 response is returned with the created [`{{crate_name}}_db::entities::tasks::Task`]'s JSON representation in the response body.
///
/// # Errors
/// - 422 Unprocessable Entity: Invalid changeset
/// - 500 Internal Server Error: Database connection failed or other generic database error
#[axum::debug_handler]
pub async fn update(
State(app_state): State<SharedAppState>,
Expand All @@ -79,7 +96,11 @@ pub async fn update(

/// Deletes a task identified by its ID from the database.
///
/// This function deletes one [`{{crate_name}}_db::entities::tasks::Task`] identified by the entity's id from the database (see [`{{crate_name}}_db::entities::tasks::delete`]) and responds with a 204 status code and empty response body. If no task is found for the ID, a 404 response is returned.
/// This function deletes one [`{{crate_name}}_db::entities::tasks::Task`] identified by the entity's id from the database (see [`{{crate_name}}_db::entities::tasks::delete`]) and responds with a 204 status code and empty response body.
///
/// # Errors
/// - 404 Not found: No task with the given ID was found.
/// - 500 Internal Server Error: Database connection failed or other generic database error
#[axum::debug_handler]
pub async fn delete(
State(app_state): State<SharedAppState>,
Expand Down
9 changes: 9 additions & 0 deletions blueprint/web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ pub mod error;
/// 3. Initialize the application state (see [`state::init_app_state`])
/// 4. Initialize the application's router (see [`routes::init_routes`])
/// 5. Boot the application and start listening for requests on the configured interface and port
///
/// # Errors
/// This function returns an Error if
/// - The environment was not found
/// - The configuration could not be loaded
/// - The server could not be bound to the specified address
pub async fn run() -> anyhow::Result<()> {
let env = get_env().context("Cannot get environment!")?;
let config: Config = load_config(&env).context("Cannot load config!")?;
Expand All @@ -51,6 +57,9 @@ pub async fn run() -> anyhow::Result<()> {
/// * registers a [`tracing_panic::panic_hook`]
///
/// The function respects the `RUST_LOG` if set or defaults to filtering spans and events with level [`tracing_subscriber::filter::LevelFilter::INFO`] and higher.
///
/// # Panics
/// This function panics if the filter could not be parsed from `RUST_LOG` and a filter could not be constructed from `info`.
pub fn init_tracing() {
let filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info"))
Expand Down
5 changes: 3 additions & 2 deletions blueprint/web/src/middlewares/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ use axum::{
use {{crate_name}}_db::entities::users;
use tracing::Span;

/// Authenticates an incoming request based on an auth token.
/// Authenticates an incoming request based on an auth token. This looks for a token in the `Authorization` header.
///
/// This looks for a token in the `Authorization` header. If no token is present or no user exists with that token (see [`{{crate_name}}_db::entities::users::load_with_token`]), a 401 response code is returned and the request is not processed further.
/// # Errors
/// If no token is present or no user exists with that token (see [`{{crate_name}}_db::entities::users::load_with_token`]), a 401 response code is returned and the request is not processed further.
#[tracing::instrument(skip_all, fields(rejection_reason = tracing::field::Empty))]
pub async fn auth(
State(app_state): State<SharedAppState>,
Expand Down
2 changes: 2 additions & 0 deletions blueprint/web/src/state.rs.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub type SharedAppState = Arc<AppState>;
/// Initializes the application state.
///
/// This function creates an [`AppState`] based on the current [`{{crate_name}}_config::Config`].
/// # Panics
/// This function panics if it cannot connect to the database.
{%- if template_type != "minimal" %}
pub async fn init_app_state(config: Config) -> AppState {
let db_pool = connect_pool(config.database)
Expand Down
6 changes: 6 additions & 0 deletions blueprint/web/src/test_helpers/mod.rs.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ impl TestRequest {
/// .header(.header(http::header::CONTENT_TYPE, "application/json"))
/// .await;
/// ```
/// # Panics
/// This function panics if the header value could not be parsed.
#[allow(unused)]
#[must_use]
pub fn header(mut self, name: HeaderName, value: &str) -> Self {
Expand Down Expand Up @@ -105,6 +107,8 @@ impl TestRequest {
}

/// Sends the request to the application under test.
/// # Panics
/// This function panics if the request could not be built.
#[allow(unused)]
pub async fn send(self) -> Response {
let mut request_builder = Request::builder().uri(&self.uri);
Expand Down Expand Up @@ -224,6 +228,8 @@ pub struct DbTestContext {
/// This function initializes a new instance of the application under test using the configuration for [`{{crate_name}}_config::Environment::Test`]. The application is configured to use the same database that is also made available to the test itself via the test context. That database is a clone of the main test database that is only used by the particular test case to ensure isolation between test cases. It is automatically torn down after the test case completes (see [`teardown`]).
///
/// This function is not invoked directly but used inside of the [`{{crate_name}}_macros::db_test`] attribute macro. The test context is automatically passed to test cases marked with that macro as an argument.
/// # Panics
/// This function panics if the `Test` configuration could not be loaded.
#[allow(unused)]
pub async fn setup() -> DbTestContext {
let init_config: OnceCell<Config> = OnceCell::new();
Expand Down