2 Commits
Author SHA1 Message Date
sbstp a1076a0731 run clippy & fix
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/fmt Pipeline was successful
2026-08-08 22:53:11 -04:00
sbstp 20c2afb5ad simplify static assets 2026-08-08 22:41:50 -04:00
6 changed files with 258 additions and 246 deletions
+27 -16
View File
@@ -48,12 +48,23 @@ fn content_hash(data: &[u8]) -> String {
hex::encode(hasher.finalize())
}
/// Constructs a `StaticAssetStore` from `name => path` pairs, embedding each
/// file's bytes at compile time via `include_bytes!`.
/// Returns the file name portion of a path, e.g. `"../static/style.css"`
/// becomes `"style.css"`. Used to derive an asset's registry key from its
/// path.
fn file_name(path: &'static str) -> &'static str {
match path.rfind('/') {
Some(i) => &path[i + 1..],
None => path,
}
}
/// Constructs a `StaticAssetStore` from paths, embedding each file's bytes at
/// compile time via `include_bytes!`. Each asset is keyed by its file name
/// (derived from the path), so there's no need to repeat the name.
macro_rules! static_assets {
($($name:literal => $path:literal),* $(,)?) => {
($($path:literal),* $(,)?) => {
StaticAssetStore::new(&[
$(($name, include_bytes!($path))),*
$((file_name($path), include_bytes!($path))),*
])
};
}
@@ -61,18 +72,18 @@ macro_rules! static_assets {
/// The app's static assets, loaded once on first use.
pub static STORE: LazyLock<StaticAssetStore> = LazyLock::new(|| {
static_assets! {
"style.css" => "../static/style.css",
"passkey-login.js" => "../static/passkey-login.js",
"passkey-register.js" => "../static/passkey-register.js",
"password-toggle.js" => "../static/password-toggle.js",
"rewards.js" => "../static/rewards.js",
"htmx.min.js" => "../static/htmx.min.js",
"idiomorph-ext.min.js" => "../static/idiomorph-ext.min.js",
"htmx-ws.min.js" => "../static/htmx-ws.min.js",
"favicon.ico" => "../static/favicon.ico",
"favicon-32x32.png" => "../static/favicon-32x32.png",
"apple-touch-icon.png" => "../static/apple-touch-icon.png",
"logo.svg" => "../static/logo.svg",
"../static/style.css",
"../static/passkey-login.js",
"../static/passkey-register.js",
"../static/password-toggle.js",
"../static/rewards.js",
"../static/htmx.min.js",
"../static/idiomorph-ext.min.js",
"../static/htmx-ws.min.js",
"../static/favicon.ico",
"../static/favicon-32x32.png",
"../static/apple-touch-icon.png",
"../static/logo.svg",
}
});
+2 -3
View File
@@ -135,7 +135,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// WebAuthn config from env vars. RP_ID must match the host users access the site from.
let rp_id = env::var("RP_ID").unwrap_or_else(|_| {
let host = public_base_url
public_base_url
.trim_start_matches("http://")
.trim_start_matches("https://")
.split('/')
@@ -144,8 +144,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.split(':')
.next()
.unwrap_or("localhost")
.to_owned();
host
.to_owned()
});
let rp_name = env::var("RP_NAME").unwrap_or_else(|_| "Sustenance".into());
let origin =
+7
View File
@@ -153,6 +153,10 @@ pub trait ItemRepository: Send + Sync {
item_id: i64,
checked: bool,
) -> DomainResult<i64>;
/// Update the mutable fields of an item. The bare field list mirrors the
/// edit form's inputs; clippy flags the argument count, which is justified
/// here by the flat domain signature.
#[allow(clippy::too_many_arguments)]
async fn update_item(
&self,
txn: &mut SqliteConnection,
@@ -276,6 +280,9 @@ pub trait MealIngredientRepository: Send + Sync {
note: String,
category_id: Option<i64>,
) -> DomainResult<i64>;
/// Update the mutable fields of a meal ingredient. Same flat signature
/// rationale as `ItemRepository::update_item`.
#[allow(clippy::too_many_arguments)]
async fn update_ingredient(
&self,
txn: &mut SqliteConnection,
+3
View File
@@ -345,6 +345,9 @@ pub struct MealService {
}
impl MealService {
/// Builds the meal service from its dependencies. The argument count is
/// intentional: it assembles the service's repository/notifier graph.
#[allow(clippy::too_many_arguments)]
pub fn new(
db: SqliteDatabase,
meals: Arc<dyn MealRepository>,
+107 -115
View File
@@ -1539,7 +1539,7 @@ mod tests {
let users = SqliteUserRepository;
let email = email.to_owned();
db.run(move |txn| {
let users = users.clone();
let users = users;
Box::pin(async move {
users
.create_user(txn, email, "Test User".into(), "hash".into())
@@ -1554,7 +1554,7 @@ mod tests {
let lists = SqliteListRepository;
let name = name.to_owned();
db.run(move |txn| {
let lists = lists.clone();
let lists = lists;
Box::pin(async move { lists.create_list(txn, name).await })
})
.await
@@ -1565,7 +1565,7 @@ mod tests {
let items = SqliteItemRepository;
let name_for_insert = name.to_owned();
db.run(move |txn| {
let items = items.clone();
let items = items;
Box::pin(async move {
items
.add_item(
@@ -1583,7 +1583,7 @@ mod tests {
.unwrap();
let items = SqliteItemRepository;
db.run(move |txn| {
let items = items.clone();
let items = items;
Box::pin(async move { items.items(txn, list_id).await })
})
.await
@@ -1596,7 +1596,7 @@ mod tests {
async fn get_items(db: &SqliteDatabase, list_id: i64) -> Vec<Item> {
let items = SqliteItemRepository;
db.run(move |txn| {
let items = items.clone();
let items = items;
Box::pin(async move { items.items(txn, list_id).await })
})
.await
@@ -1606,7 +1606,7 @@ mod tests {
async fn get_categories(db: &SqliteDatabase) -> Vec<Category> {
let categories = SqliteCategoryRepository;
db.run(move |txn| {
let categories = categories.clone();
let categories = categories;
Box::pin(async move { categories.categories(txn).await })
})
.await
@@ -1631,7 +1631,7 @@ mod tests {
let users = SqliteUserRepository;
let result = db
.run(move |txn| {
let users = users.clone();
let users = users;
Box::pin(async move {
users
.create_user(
@@ -1654,7 +1654,7 @@ mod tests {
let users = SqliteUserRepository;
let found = db
.run(move |txn| {
let users = users.clone();
let users = users;
Box::pin(async move {
users
.find_user_by_email(txn, "alice@example.com".into())
@@ -1675,7 +1675,7 @@ mod tests {
let users = SqliteUserRepository;
let found = db
.run(move |txn| {
let users = users.clone();
let users = users;
Box::pin(async move {
users
.find_user_by_email(txn, "ALICE@EXAMPLE.COM".into())
@@ -1693,7 +1693,7 @@ mod tests {
let users = SqliteUserRepository;
let found = db
.run(move |txn| {
let users = users.clone();
let users = users;
Box::pin(async move {
users
.find_user_by_email(txn, "nobody@example.com".into())
@@ -1711,7 +1711,7 @@ mod tests {
let users = SqliteUserRepository;
let empty = db
.run(move |txn| {
let users = users.clone();
let users = users;
Box::pin(async move { users.has_users(txn).await })
})
.await
@@ -1721,7 +1721,7 @@ mod tests {
create_user(&db, "alice@example.com").await;
let has = db
.run(move |txn| {
let users = users.clone();
let users = users;
Box::pin(async move { users.has_users(txn).await })
})
.await
@@ -1738,7 +1738,7 @@ mod tests {
let sessions = SqliteSessionRepository;
let (token, csrf) = db
.run(move |txn| {
let sessions = sessions.clone();
let sessions = sessions;
Box::pin(async move { sessions.create_session(txn, user.id).await })
})
.await
@@ -1748,7 +1748,7 @@ mod tests {
let session = db
.run(move |txn| {
let sessions = sessions.clone();
let sessions = sessions;
Box::pin(async move { sessions.session_user(txn, token.clone()).await })
})
.await
@@ -1764,7 +1764,7 @@ mod tests {
let sessions = SqliteSessionRepository;
let session = db
.run(move |txn| {
let sessions = sessions.clone();
let sessions = sessions;
Box::pin(async move { sessions.session_user(txn, "bogus".into()).await })
})
.await
@@ -1779,7 +1779,7 @@ mod tests {
let sessions = SqliteSessionRepository;
let (token, _) = db
.run(move |txn| {
let sessions = sessions.clone();
let sessions = sessions;
Box::pin(async move { sessions.create_session(txn, user.id).await })
})
.await
@@ -1787,7 +1787,7 @@ mod tests {
let token_for_delete = token.clone();
db.run(move |txn| {
let sessions = sessions.clone();
let sessions = sessions;
Box::pin(async move { sessions.delete_session(txn, token_for_delete).await })
})
.await
@@ -1795,7 +1795,7 @@ mod tests {
let session = db
.run(move |txn| {
let sessions = sessions.clone();
let sessions = sessions;
Box::pin(async move { sessions.session_user(txn, token).await })
})
.await
@@ -1825,7 +1825,7 @@ mod tests {
let lists = SqliteListRepository;
let summaries = db
.run(move |txn| {
let lists = lists.clone();
let lists = lists;
Box::pin(async move { lists.list_summaries(txn).await })
})
.await
@@ -1843,7 +1843,7 @@ mod tests {
let list_id = list.id;
db.run(move |txn| {
let lists = lists.clone();
let lists = lists;
Box::pin(async move { lists.set_archived(txn, list_id, true).await })
})
.await
@@ -1851,7 +1851,7 @@ mod tests {
let summaries = db
.run(move |txn| {
let lists = lists.clone();
let lists = lists;
Box::pin(async move { lists.list_summaries(txn).await })
})
.await
@@ -1860,7 +1860,7 @@ mod tests {
let archived = db
.run(move |txn| {
let lists = lists.clone();
let lists = lists;
Box::pin(async move { lists.list_archived_summaries(txn).await })
})
.await
@@ -1878,13 +1878,13 @@ mod tests {
let list_id = list.id;
db.run(move |txn| {
let lists = lists.clone();
let lists = lists;
Box::pin(async move { lists.set_archived(txn, list_id, true).await })
})
.await
.unwrap();
db.run(move |txn| {
let lists = lists.clone();
let lists = lists;
Box::pin(async move { lists.set_archived(txn, list_id, false).await })
})
.await
@@ -1892,7 +1892,7 @@ mod tests {
let summaries = db
.run(move |txn| {
let lists = lists.clone();
let lists = lists;
Box::pin(async move { lists.list_summaries(txn).await })
})
.await
@@ -1907,7 +1907,7 @@ mod tests {
let lists = SqliteListRepository;
let result = db
.run(move |txn| {
let lists = lists.clone();
let lists = lists;
Box::pin(async move { lists.set_archived(txn, 9999, true).await })
})
.await;
@@ -1921,7 +1921,7 @@ mod tests {
let lists = SqliteListRepository;
let found = db
.run(move |txn| {
let lists = lists.clone();
let lists = lists;
Box::pin(async move { lists.get_list(txn, list.id).await })
})
.await
@@ -1930,7 +1930,7 @@ mod tests {
let missing = db
.run(move |txn| {
let lists = lists.clone();
let lists = lists;
Box::pin(async move { lists.get_list(txn, 9999).await })
})
.await
@@ -1946,7 +1946,7 @@ mod tests {
let categories = SqliteCategoryRepository;
let id = db
.run(move |txn| {
let categories = categories.clone();
let categories = categories;
Box::pin(async move { categories.create_category(txn, "Bakery".into()).await })
})
.await
@@ -1963,7 +1963,7 @@ mod tests {
let categories = SqliteCategoryRepository;
let result = db
.run(move |txn| {
let categories = categories.clone();
let categories = categories;
Box::pin(async move { categories.create_category(txn, "Produce".into()).await })
})
.await;
@@ -1975,7 +1975,7 @@ mod tests {
async fn get_meal_categories(db: &SqliteDatabase) -> Vec<MealCategory> {
let categories = SqliteMealCategoryRepository;
db.run(move |txn| {
let categories = categories.clone();
let categories = categories;
Box::pin(async move { categories.meal_categories(txn).await })
})
.await
@@ -2004,7 +2004,7 @@ mod tests {
let categories = SqliteMealCategoryRepository;
let id = db
.run(move |txn| {
let categories = categories.clone();
let categories = categories;
Box::pin(async move {
categories
.create_meal_category(txn, "Breakfast".into())
@@ -2024,7 +2024,7 @@ mod tests {
let categories = SqliteMealCategoryRepository;
let result = db
.run(move |txn| {
let categories = categories.clone();
let categories = categories;
Box::pin(async move { categories.create_meal_category(txn, "Beef".into()).await })
})
.await;
@@ -2037,7 +2037,7 @@ mod tests {
let categories = SqliteMealCategoryRepository;
let category_id = db
.run(move |txn| {
let categories = categories.clone();
let categories = categories;
Box::pin(async move {
categories
.create_meal_category(txn, "Breakfast".into())
@@ -2049,7 +2049,7 @@ mod tests {
let meal = create_meal(&db, "Pancakes").await;
let meals = SqliteMealRepository;
db.run(move |txn| {
let meals = meals.clone();
let meals = meals;
Box::pin(async move {
meals
.update_meal(
@@ -2066,7 +2066,7 @@ mod tests {
.unwrap();
db.run(move |txn| {
let categories = categories.clone();
let categories = categories;
Box::pin(async move { categories.delete_meal_category(txn, category_id).await })
})
.await
@@ -2074,7 +2074,7 @@ mod tests {
let fetched = db
.run(move |txn| {
let meals = meals.clone();
let meals = meals;
Box::pin(async move { meals.get_meal(txn, meal.id).await })
})
.await
@@ -2092,7 +2092,7 @@ mod tests {
let items = SqliteItemRepository;
let revision = db
.run(move |txn| {
let items = items.clone();
let items = items;
Box::pin(async move {
items
.add_item(
@@ -2126,7 +2126,7 @@ mod tests {
let items = SqliteItemRepository;
let result = db
.run(move |txn| {
let items = items.clone();
let items = items;
Box::pin(async move {
items
.add_item(
@@ -2167,7 +2167,7 @@ mod tests {
];
let revision = db
.run(move |txn| {
let items = items.clone();
let items = items;
Box::pin(async move { items.add_items_bulk(txn, list.id, new_items).await })
})
.await
@@ -2188,7 +2188,7 @@ mod tests {
let items = SqliteItemRepository;
let revision = db
.run(move |txn| {
let items = items.clone();
let items = items;
Box::pin(async move {
items
.update_item(
@@ -2221,7 +2221,7 @@ mod tests {
let items = SqliteItemRepository;
let result = db
.run(move |txn| {
let items = items.clone();
let items = items;
Box::pin(async move {
items
.update_item(
@@ -2248,7 +2248,7 @@ mod tests {
let items = SqliteItemRepository;
let revision = db
.run(move |txn| {
let items = items.clone();
let items = items;
Box::pin(async move { items.delete_item(txn, list.id, item.id).await })
})
.await
@@ -2264,7 +2264,7 @@ mod tests {
let items = SqliteItemRepository;
let result = db
.run(move |txn| {
let items = items.clone();
let items = items;
Box::pin(async move { items.delete_item(txn, list.id, 9999).await })
})
.await;
@@ -2278,7 +2278,7 @@ mod tests {
let items = SqliteItemRepository;
let result = db
.run(move |txn| {
let items = items.clone();
let items = items;
Box::pin(async move { items.set_item_checked(txn, list.id, 9999, true).await })
})
.await;
@@ -2294,7 +2294,7 @@ mod tests {
let invitations = SqliteInvitationRepository;
let expires = db
.run(move |txn| {
let invitations = invitations.clone();
let invitations = invitations;
Box::pin(async move {
invitations
.create_invitation(txn, user.id, "token-1".into())
@@ -2307,7 +2307,7 @@ mod tests {
let valid = db
.run(move |txn| {
let invitations = invitations.clone();
let invitations = invitations;
Box::pin(async move { invitations.invitation(txn, "token-1".into()).await })
})
.await
@@ -2321,7 +2321,7 @@ mod tests {
let invitations = SqliteInvitationRepository;
let valid = db
.run(move |txn| {
let invitations = invitations.clone();
let invitations = invitations;
Box::pin(async move { invitations.invitation(txn, "bogus".into()).await })
})
.await
@@ -2335,7 +2335,7 @@ mod tests {
let user = create_user(&db, "alice@example.com").await;
let invitations = SqliteInvitationRepository;
db.run(move |txn| {
let invitations = invitations.clone();
let invitations = invitations;
Box::pin(async move {
invitations
.create_invitation(txn, user.id, "token-1".into())
@@ -2346,7 +2346,7 @@ mod tests {
.unwrap();
db.run(move |txn| {
let invitations = invitations.clone();
let invitations = invitations;
Box::pin(async move { invitations.accept_invitation(txn, "token-1".into()).await })
})
.await
@@ -2354,7 +2354,7 @@ mod tests {
let valid = db
.run(move |txn| {
let invitations = invitations.clone();
let invitations = invitations;
Box::pin(async move { invitations.invitation(txn, "token-1".into()).await })
})
.await
@@ -2368,7 +2368,7 @@ mod tests {
let invitations = SqliteInvitationRepository;
let result = db
.run(move |txn| {
let invitations = invitations.clone();
let invitations = invitations;
Box::pin(async move { invitations.accept_invitation(txn, "bogus".into()).await })
})
.await;
@@ -2384,13 +2384,13 @@ mod tests {
let item = add_item(&db, list.id, "Coffee").await;
let items = SqliteItemRepository;
db.run(move |txn| {
let items = items.clone();
let items = items;
Box::pin(async move { items.set_item_checked(txn, list.id, item.id, true).await })
})
.await
.unwrap();
db.run(move |txn| {
let items = items.clone();
let items = items;
Box::pin(async move { items.set_item_checked(txn, list.id, item.id, true).await })
})
.await
@@ -2411,7 +2411,7 @@ mod tests {
let first_item = get_items(&db, list.id).await.remove(0);
db.run(move |txn| {
let items = items.clone();
let items = items;
Box::pin(async move {
items
.set_item_checked(txn, list.id, first_item.id, true)
@@ -2433,7 +2433,7 @@ mod tests {
let meals = SqliteMealRepository;
let name = name.to_owned();
db.run(move |txn| {
let meals = meals.clone();
let meals = meals;
Box::pin(async move { meals.create_meal(txn, name, String::new(), None).await })
})
.await
@@ -2449,7 +2449,7 @@ mod tests {
let ingredients = SqliteMealIngredientRepository;
let name_for_insert = name.to_owned();
db.run(move |txn| {
let ingredients = ingredients.clone();
let ingredients = ingredients;
Box::pin(async move {
ingredients
.add_ingredient(
@@ -2467,7 +2467,7 @@ mod tests {
.unwrap();
let ingredients = SqliteMealIngredientRepository;
db.run(move |txn| {
let ingredients = ingredients.clone();
let ingredients = ingredients;
Box::pin(async move { ingredients.ingredients_for_meal(txn, meal_id).await })
})
.await
@@ -2494,7 +2494,7 @@ mod tests {
let meals = SqliteMealRepository;
let meals = db
.run(move |txn| {
let meals = meals.clone();
let meals = meals;
Box::pin(async move { meals.list_meals(txn, "").await })
})
.await
@@ -2511,12 +2511,8 @@ mod tests {
create_meal(&db, "Chicken Curry").await;
let meals = SqliteMealRepository;
let names = |posted_query: &str| {
let meals = meals.clone();
let posted_query = posted_query.to_owned();
db.run(move |txn| {
let meals = meals.clone();
Box::pin(async move { meals.list_meals(txn, &posted_query).await })
})
db.run(move |txn| Box::pin(async move { meals.list_meals(txn, &posted_query).await }))
};
let matched = names("pasta").await.unwrap();
let matched_names = matched.iter().map(|m| m.name.as_str()).collect::<Vec<_>>();
@@ -2531,12 +2527,8 @@ mod tests {
create_meal(&db, "Salad").await;
let meals = SqliteMealRepository;
let names = |posted_query: &str| {
let meals = meals.clone();
let posted_query = posted_query.to_owned();
db.run(move |txn| {
let meals = meals.clone();
Box::pin(async move { meals.list_meals(txn, &posted_query).await })
})
db.run(move |txn| Box::pin(async move { meals.list_meals(txn, &posted_query).await }))
};
// A bare `%` must not act as a wildcard matching every meal: it only
// matches meals that contain a literal `%` character.
@@ -2558,7 +2550,7 @@ mod tests {
let meals = SqliteMealRepository;
let fetched = db
.run(move |txn| {
let meals = meals.clone();
let meals = meals;
Box::pin(async move { meals.get_meal(txn, meal.id).await })
})
.await
@@ -2580,7 +2572,7 @@ mod tests {
let meals = SqliteMealRepository;
let found = db
.run(move |txn| {
let meals = meals.clone();
let meals = meals;
Box::pin(async move { meals.get_meal(txn, 9999).await })
})
.await
@@ -2594,7 +2586,7 @@ mod tests {
let meal = create_meal(&db, "Pasta").await;
let meals = SqliteMealRepository;
db.run(move |txn| {
let meals = meals.clone();
let meals = meals;
Box::pin(async move {
meals
.update_meal(
@@ -2611,7 +2603,7 @@ mod tests {
.unwrap();
let fetched = db
.run(move |txn| {
let meals = meals.clone();
let meals = meals;
Box::pin(async move { meals.get_meal(txn, meal.id).await })
})
.await
@@ -2627,7 +2619,7 @@ mod tests {
let meals = SqliteMealRepository;
let result = db
.run(move |txn| {
let meals = meals.clone();
let meals = meals;
Box::pin(async move {
meals
.update_meal(txn, 9999, "X".into(), String::new(), None)
@@ -2645,14 +2637,14 @@ mod tests {
add_ingredient(&db, meal.id, "Penne", None).await;
let meals = SqliteMealRepository;
db.run(move |txn| {
let meals = meals.clone();
let meals = meals;
Box::pin(async move { meals.delete_meal(txn, meal.id).await })
})
.await
.unwrap();
let found = db
.run(move |txn| {
let meals = meals.clone();
let meals = meals;
Box::pin(async move { meals.get_meal(txn, meal.id).await })
})
.await
@@ -2666,7 +2658,7 @@ mod tests {
let meals = SqliteMealRepository;
let result = db
.run(move |txn| {
let meals = meals.clone();
let meals = meals;
Box::pin(async move { meals.delete_meal(txn, 9999).await })
})
.await;
@@ -2680,7 +2672,7 @@ mod tests {
let ingredients = SqliteMealIngredientRepository;
let result = db
.run(move |txn| {
let ingredients = ingredients.clone();
let ingredients = ingredients;
Box::pin(async move {
ingredients
.add_ingredient(
@@ -2705,7 +2697,7 @@ mod tests {
let ingredient = add_ingredient(&db, meal.id, "Penne", None).await;
let ingredients = SqliteMealIngredientRepository;
db.run(move |txn| {
let ingredients = ingredients.clone();
let ingredients = ingredients;
Box::pin(async move {
ingredients
.update_ingredient(
@@ -2724,7 +2716,7 @@ mod tests {
.unwrap();
let fetched = db
.run(move |txn| {
let ingredients = ingredients.clone();
let ingredients = ingredients;
Box::pin(async move { ingredients.ingredients_for_meal(txn, meal.id).await })
})
.await
@@ -2740,7 +2732,7 @@ mod tests {
let ingredient = add_ingredient(&db, meal.id, "Penne", None).await;
let ingredients = SqliteMealIngredientRepository;
db.run(move |txn| {
let ingredients = ingredients.clone();
let ingredients = ingredients;
Box::pin(async move {
ingredients
.delete_ingredient(txn, meal.id, ingredient.id)
@@ -2751,7 +2743,7 @@ mod tests {
.unwrap();
let remaining = db
.run(move |txn| {
let ingredients = ingredients.clone();
let ingredients = ingredients;
Box::pin(async move { ingredients.ingredients_for_meal(txn, meal.id).await })
})
.await
@@ -2766,7 +2758,7 @@ mod tests {
let ingredients = SqliteMealIngredientRepository;
let result = db
.run(move |txn| {
let ingredients = ingredients.clone();
let ingredients = ingredients;
Box::pin(async move { ingredients.delete_ingredient(txn, meal.id, 9999).await })
})
.await;
@@ -2781,7 +2773,7 @@ mod tests {
let meal_id = meal.id;
let id = db
.run(move |txn| {
let list_meals = list_meals.clone();
let list_meals = list_meals;
Box::pin(
async move { list_meals.add_meal(txn, list_id, Some(meal_id), name).await },
)
@@ -2806,7 +2798,7 @@ mod tests {
let list_meals = SqliteListMealRepository;
let meals = db
.run(move |txn| {
let list_meals = list_meals.clone();
let list_meals = list_meals;
Box::pin(async move { list_meals.list_meals(txn, list.id).await })
})
.await
@@ -2829,7 +2821,7 @@ mod tests {
let ingredients = SqliteMealIngredientRepository;
let ingredient_rows = db
.run(move |txn| {
let ingredients = ingredients.clone();
let ingredients = ingredients;
Box::pin(async move { ingredients.ingredients_for_meal(txn, meal.id).await })
})
.await
@@ -2845,7 +2837,7 @@ mod tests {
})
.collect();
db.run(move |txn| {
let items = items.clone();
let items = items;
Box::pin(async move { items.add_items_bulk(txn, list.id, new_items).await })
})
.await
@@ -2855,7 +2847,7 @@ mod tests {
let list_meals = SqliteListMealRepository;
let revision = db
.run(move |txn| {
let list_meals = list_meals.clone();
let list_meals = list_meals;
Box::pin(async move { list_meals.remove_meal(txn, list.id, list_meal.id).await })
})
.await
@@ -2864,7 +2856,7 @@ mod tests {
let meals = db
.run(move |txn| {
let list_meals = list_meals.clone();
let list_meals = list_meals;
Box::pin(async move { list_meals.list_meals(txn, list.id).await })
})
.await
@@ -2880,7 +2872,7 @@ mod tests {
let list_meals = SqliteListMealRepository;
let result = db
.run(move |txn| {
let list_meals = list_meals.clone();
let list_meals = list_meals;
Box::pin(async move { list_meals.remove_meal(txn, list.id, 9999).await })
})
.await;
@@ -2899,7 +2891,7 @@ mod tests {
let list_meals = SqliteListMealRepository;
let (copied, revision) = db
.run(move |txn| {
let list_meals = list_meals.clone();
let list_meals = list_meals;
Box::pin(async move {
list_meals
.copy_meals_to_list(txn, source.id, dest.id, &[list_meal.id])
@@ -2919,7 +2911,7 @@ mod tests {
// The source meal is untouched.
let source_meals = db
.run(move |txn| {
let list_meals = list_meals.clone();
let list_meals = list_meals;
Box::pin(async move { list_meals.list_meals(txn, source.id).await })
})
.await
@@ -2939,7 +2931,7 @@ mod tests {
// becomes NULL.
let meals = SqliteMealRepository;
db.run(move |txn| {
let meals = meals.clone();
let meals = meals;
Box::pin(async move { meals.delete_meal(txn, meal.id).await })
})
.await
@@ -2948,7 +2940,7 @@ mod tests {
let list_meals = SqliteListMealRepository;
let (copied, _) = db
.run(move |txn| {
let list_meals = list_meals.clone();
let list_meals = list_meals;
Box::pin(async move {
list_meals
.copy_meals_to_list(txn, source.id, dest.id, &[list_meal.id])
@@ -2970,7 +2962,7 @@ mod tests {
let list_meals = SqliteListMealRepository;
let result = db
.run(move |txn| {
let list_meals = list_meals.clone();
let list_meals = list_meals;
Box::pin(async move {
list_meals
.copy_meals_to_list(txn, source.id, dest.id, &[9999])
@@ -2991,7 +2983,7 @@ mod tests {
let created = db
.run(move |txn| {
let passkeys = passkeys.clone();
let passkeys = passkeys;
Box::pin(async move {
passkeys
.create_passkey(txn, user.id, "cred-1".into(), "{}".into(), 0)
@@ -3006,7 +2998,7 @@ mod tests {
let found = db
.run(move |txn| {
let passkeys = passkeys.clone();
let passkeys = passkeys;
Box::pin(async move { passkeys.find_by_credential_id(txn, "cred-1".into()).await })
})
.await
@@ -3016,7 +3008,7 @@ mod tests {
let listed = db
.run(move |txn| {
let passkeys = passkeys.clone();
let passkeys = passkeys;
Box::pin(async move { passkeys.list_for_user(txn, user.id).await })
})
.await
@@ -3024,7 +3016,7 @@ mod tests {
assert_eq!(listed.len(), 1);
db.run(move |txn| {
let passkeys = passkeys.clone();
let passkeys = passkeys;
Box::pin(async move { passkeys.delete_passkey(txn, user.id, created.id).await })
})
.await
@@ -3032,7 +3024,7 @@ mod tests {
let after = db
.run(move |txn| {
let passkeys = passkeys.clone();
let passkeys = passkeys;
Box::pin(async move { passkeys.list_for_user(txn, user.id).await })
})
.await
@@ -3046,7 +3038,7 @@ mod tests {
let user = create_user(&db, "alice@example.com").await;
let passkeys = SqlitePasskeyRepository;
db.run(move |txn| {
let passkeys = passkeys.clone();
let passkeys = passkeys;
Box::pin(async move {
passkeys
.create_passkey(txn, user.id, "cred-1".into(), "{}".into(), 0)
@@ -3058,7 +3050,7 @@ mod tests {
let result = db
.run(move |txn| {
let passkeys = passkeys.clone();
let passkeys = passkeys;
Box::pin(async move {
passkeys
.create_passkey(txn, user.id, "cred-1".into(), "{}".into(), 0)
@@ -3076,7 +3068,7 @@ mod tests {
let passkeys = SqlitePasskeyRepository;
let result = db
.run(move |txn| {
let passkeys = passkeys.clone();
let passkeys = passkeys;
Box::pin(async move { passkeys.delete_passkey(txn, user.id, 9999).await })
})
.await;
@@ -3093,7 +3085,7 @@ mod tests {
let created = db
.run(move |txn| {
let cards = cards.clone();
let cards = cards;
Box::pin(async move {
cards
.create_card(
@@ -3116,7 +3108,7 @@ mod tests {
let listed = db
.run(move |txn| {
let cards = cards.clone();
let cards = cards;
Box::pin(async move { cards.list_cards(txn, user.id).await })
})
.await
@@ -3125,7 +3117,7 @@ mod tests {
assert_eq!(listed[0].id, created.id);
db.run(move |txn| {
let cards = cards.clone();
let cards = cards;
Box::pin(async move { cards.delete_card(txn, user.id, created.id).await })
})
.await
@@ -3133,7 +3125,7 @@ mod tests {
let after = db
.run(move |txn| {
let cards = cards.clone();
let cards = cards;
Box::pin(async move { cards.list_cards(txn, user.id).await })
})
.await
@@ -3149,7 +3141,7 @@ mod tests {
let cards = SqliteRewardsCardRepository;
db.run(move |txn| {
let cards = cards.clone();
let cards = cards;
Box::pin(async move {
cards
.create_card(
@@ -3167,7 +3159,7 @@ mod tests {
let bobs = db
.run(move |txn| {
let cards = cards.clone();
let cards = cards;
Box::pin(async move { cards.list_cards(txn, bob.id).await })
})
.await
@@ -3177,7 +3169,7 @@ mod tests {
// Bob cannot delete Alice's card.
let result = db
.run(move |txn| {
let cards = cards.clone();
let cards = cards;
Box::pin(async move { cards.delete_card(txn, bob.id, 1).await })
})
.await;
@@ -3191,7 +3183,7 @@ mod tests {
let cards = SqliteRewardsCardRepository;
let result = db
.run(move |txn| {
let cards = cards.clone();
let cards = cards;
Box::pin(async move { cards.delete_card(txn, user.id, 9999).await })
})
.await;
@@ -3207,7 +3199,7 @@ mod tests {
let created = db
.run(move |txn| {
let cards = cards.clone();
let cards = cards;
Box::pin(async move {
cards
.create_card(
@@ -3226,7 +3218,7 @@ mod tests {
// The owner can fetch their card.
let found = db
.run(move |txn| {
let cards = cards.clone();
let cards = cards;
Box::pin(async move { cards.get_card(txn, alice.id, created.id).await })
})
.await
@@ -3237,7 +3229,7 @@ mod tests {
// Another user cannot fetch it.
let not_found = db
.run(move |txn| {
let cards = cards.clone();
let cards = cards;
Box::pin(async move { cards.get_card(txn, bob.id, created.id).await })
})
.await
@@ -3247,7 +3239,7 @@ mod tests {
// A missing id returns None.
let missing = db
.run(move |txn| {
let cards = cards.clone();
let cards = cards;
Box::pin(async move { cards.get_card(txn, alice.id, 9999).await })
})
.await
+112 -112
View File
@@ -802,116 +802,6 @@ fn render_markdown(source: &str) -> Markup {
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Extracts the rendered SVG width from `viewBox="0 0 <width> <height>"`.
fn svg_width(svg: &str) -> u32 {
let viewbox = svg
.split("viewBox=\"")
.nth(1)
.expect("expected viewBox")
.split('"')
.next()
.unwrap();
let width = viewbox.split_whitespace().nth(2).expect("expected width");
width.parse().expect("expected numeric width")
}
#[test]
fn render_markdown_turns_bullets_into_list_html() {
let html = render_markdown("- one\n- two\n").into_string();
assert!(html.contains("<ul>"), "expected <ul>, got: {html}");
assert!(html.contains("<li>"), "expected <li>, got: {html}");
assert!(!html.contains("* one"), "raw bullet leaked through: {html}");
}
#[test]
fn render_markdown_renders_emphasis() {
let html = render_markdown("**bold** and *italic*").into_string();
assert!(html.contains("<strong>"), "expected <strong>, got: {html}");
assert!(html.contains("<em>"), "expected <em>, got: {html}");
}
#[test]
fn format_date_renders_human_readable_date() {
// 2026-08-07T00:00:00Z in Unix seconds.
let ts = 1_786_060_800;
assert_eq!(format_date(ts), "7 Aug 2026");
}
#[test]
fn barcode_svg_renders_code128_svg() {
let html = barcode_svg("code128", "601123456789").into_string();
assert!(html.contains("<svg"), "expected svg, got: {html}");
assert!(html.contains("</svg>"), "expected closing svg, got: {html}");
}
#[test]
fn code128_uses_set_c_for_even_digit_numbers() {
// Set C (Ć) packs two digits per symbol, so an even-length digit-only
// number should be encoded with set C rather than set B.
assert_eq!(code128_input("601123456789"), "Ć601123456789");
// Non-numeric data falls back to set B.
assert_eq!(code128_input("ABC123"), "ƁABC123");
assert_eq!(code128_input(""), "Ɓ");
}
#[test]
fn code128_odd_digit_numbers_lead_with_set_b_then_switch_to_set_c() {
// 21-digit number: first digit in set B, then set C for the rest.
assert_eq!(
code128_input("606171584511340224537"),
"Ɓ6Ć06171584511340224537"
);
}
#[test]
fn code128_set_c_renders_shorter_than_set_b() {
let number = "601123456789";
let set_c = barcode_svg("code128", number).into_string();
// Force set B by using a non-numeric character so the digit-only
// fast path doesn't kick in.
let set_b = barcode_svg("code128", &format!("{number} ")).into_string();
let width_c = svg_width(&set_c);
let width_b = svg_width(&set_b);
assert!(
width_c < width_b,
"expected set C ({width_c}) narrower than set B ({width_b})"
);
}
#[test]
fn code128_odd_digit_number_renders_shorter_than_pure_set_b() {
let number = "606171584511340224537";
let mixed = barcode_svg("code128", number).into_string();
// Force pure set B by appending a non-numeric character.
let set_b = barcode_svg("code128", &format!("{number} ")).into_string();
let width_mixed = svg_width(&mixed);
let width_b = svg_width(&set_b);
assert!(
width_mixed < width_b,
"expected mixed ({width_mixed}) narrower than set B ({width_b})"
);
}
#[test]
fn barcode_svg_renders_code39_svg() {
let html = barcode_svg("code39", "ABC123").into_string();
assert!(html.contains("<svg"), "expected svg, got: {html}");
}
#[test]
fn barcode_svg_falls_back_to_text_for_invalid_number() {
// Code 39 only supports uppercase letters and digits, so lowercase
// input cannot be encoded and falls back to plain text.
let html = barcode_svg("code39", "abc").into_string();
assert!(!html.contains("<svg"), "expected no svg, got: {html}");
assert!(html.contains("abc"), "expected fallback text, got: {html}");
}
}
pub fn list_page(
user: &User,
list: &GroceryList,
@@ -1596,7 +1486,7 @@ fn barcode_svg(symbology: &str, number: &str) -> Markup {
.generate(&encoded)
.ok()
}),
_ => barcoders::sym::code128::Code128::new(&code128_input(number))
_ => barcoders::sym::code128::Code128::new(code128_input(number))
.ok()
.map(|code| code.encode())
.and_then(|encoded| {
@@ -1628,7 +1518,7 @@ fn code128_input(number: &str) -> String {
return format!("Ɓ{number}");
}
let len = number.chars().count();
if len % 2 == 0 {
if len.is_multiple_of(2) {
format!("Ć{number}")
} else {
let (first, rest) = number.split_at(1);
@@ -1723,3 +1613,113 @@ fn initials(name: &str) -> String {
.collect::<String>()
.to_uppercase()
}
#[cfg(test)]
mod tests {
use super::*;
/// Extracts the rendered SVG width from `viewBox="0 0 <width> <height>"`.
fn svg_width(svg: &str) -> u32 {
let viewbox = svg
.split("viewBox=\"")
.nth(1)
.expect("expected viewBox")
.split('"')
.next()
.unwrap();
let width = viewbox.split_whitespace().nth(2).expect("expected width");
width.parse().expect("expected numeric width")
}
#[test]
fn render_markdown_turns_bullets_into_list_html() {
let html = render_markdown("- one\n- two\n").into_string();
assert!(html.contains("<ul>"), "expected <ul>, got: {html}");
assert!(html.contains("<li>"), "expected <li>, got: {html}");
assert!(!html.contains("* one"), "raw bullet leaked through: {html}");
}
#[test]
fn render_markdown_renders_emphasis() {
let html = render_markdown("**bold** and *italic*").into_string();
assert!(html.contains("<strong>"), "expected <strong>, got: {html}");
assert!(html.contains("<em>"), "expected <em>, got: {html}");
}
#[test]
fn format_date_renders_human_readable_date() {
// 2026-08-07T00:00:00Z in Unix seconds.
let ts = 1_786_060_800;
assert_eq!(format_date(ts), "7 Aug 2026");
}
#[test]
fn barcode_svg_renders_code128_svg() {
let html = barcode_svg("code128", "601123456789").into_string();
assert!(html.contains("<svg"), "expected svg, got: {html}");
assert!(html.contains("</svg>"), "expected closing svg, got: {html}");
}
#[test]
fn code128_uses_set_c_for_even_digit_numbers() {
// Set C (Ć) packs two digits per symbol, so an even-length digit-only
// number should be encoded with set C rather than set B.
assert_eq!(code128_input("601123456789"), "Ć601123456789");
// Non-numeric data falls back to set B.
assert_eq!(code128_input("ABC123"), "ƁABC123");
assert_eq!(code128_input(""), "Ɓ");
}
#[test]
fn code128_odd_digit_numbers_lead_with_set_b_then_switch_to_set_c() {
// 21-digit number: first digit in set B, then set C for the rest.
assert_eq!(
code128_input("606171584511340224537"),
"Ɓ6Ć06171584511340224537"
);
}
#[test]
fn code128_set_c_renders_shorter_than_set_b() {
let number = "601123456789";
let set_c = barcode_svg("code128", number).into_string();
// Force set B by using a non-numeric character so the digit-only
// fast path doesn't kick in.
let set_b = barcode_svg("code128", &format!("{number} ")).into_string();
let width_c = svg_width(&set_c);
let width_b = svg_width(&set_b);
assert!(
width_c < width_b,
"expected set C ({width_c}) narrower than set B ({width_b})"
);
}
#[test]
fn code128_odd_digit_number_renders_shorter_than_pure_set_b() {
let number = "606171584511340224537";
let mixed = barcode_svg("code128", number).into_string();
// Force pure set B by appending a non-numeric character.
let set_b = barcode_svg("code128", &format!("{number} ")).into_string();
let width_mixed = svg_width(&mixed);
let width_b = svg_width(&set_b);
assert!(
width_mixed < width_b,
"expected mixed ({width_mixed}) narrower than set B ({width_b})"
);
}
#[test]
fn barcode_svg_renders_code39_svg() {
let html = barcode_svg("code39", "ABC123").into_string();
assert!(html.contains("<svg"), "expected svg, got: {html}");
}
#[test]
fn barcode_svg_falls_back_to_text_for_invalid_number() {
// Code 39 only supports uppercase letters and digits, so lowercase
// input cannot be encoded and falls back to plain text.
let html = barcode_svg("code39", "abc").into_string();
assert!(!html.contains("<svg"), "expected no svg, got: {html}");
assert!(html.contains("abc"), "expected fallback text, got: {html}");
}
}