Skip to content

Commit

Permalink
Merge pull request #144 from ubuntu/rewrite-wiring-up
Browse files Browse the repository at this point in the history
refactor: wiring in the application layer
  • Loading branch information
sminez authored Nov 21, 2024
2 parents 4e9a392 + ed61a26 commit 4571f30
Show file tree
Hide file tree
Showing 17 changed files with 517 additions and 461 deletions.
2 changes: 0 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions crates/ratings_new/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,9 @@ tonic = "0.12.2"
tonic-reflection = "0.12.2"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
ratings = {path = "../ratings"}

[dev-dependencies]
simple_test_case = "1.2.0"

[build-dependencies]
git2 = { version = "0.18.2", default-features = false }
tonic-build = { version = "0.11", features = ["prost"] }
7 changes: 0 additions & 7 deletions crates/ratings_new/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
use dotenvy::dotenv;
use secrecy::SecretString;
use serde::Deserialize;
use tokio::sync::OnceCell;

static CONFIG: OnceCell<Config> = OnceCell::const_new();

/// Configuration for the general app center ratings backend service.
#[derive(Deserialize, Debug, Clone)]
Expand Down Expand Up @@ -35,10 +32,6 @@ impl Config {
envy::prefixed("APP_").from_env::<Config>()
}

pub async fn get() -> envy::Result<&'static Config> {
CONFIG.get_or_try_init(|| async { Config::load() }).await
}

/// Return a [`String`] representing the socket to run the service on
pub fn socket(&self) -> String {
let Config { port, host, .. } = self;
Expand Down
12 changes: 11 additions & 1 deletion crates/ratings_new/src/db/categories.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
use crate::db::Result;
use sqlx::{PgConnection, Postgres, QueryBuilder};

#[derive(Debug, Clone, Copy, PartialEq, Eq, sqlx::Type, strum::EnumString, strum::Display)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
sqlx::Type,
strum::EnumString,
strum::Display,
strum::FromRepr,
)]
#[repr(i32)]
pub enum Category {
ArtAndDesign = 0,
Expand Down
66 changes: 0 additions & 66 deletions crates/ratings_new/src/db/migrator.rs

This file was deleted.

57 changes: 24 additions & 33 deletions crates/ratings_new/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ use thiserror::Error;
use tokio::sync::OnceCell;
use tracing::info;

pub mod categories;
pub mod user;
pub mod vote;
mod categories;
mod user;
mod vote;

mod migrator;
use migrator::Migrator;
pub use categories::{set_categories_for_snap, snap_has_categories, Category};
pub use user::User;
pub use vote::{Timeframe, Vote, VoteSummary};

#[macro_export]
macro_rules! conn {
Expand All @@ -21,27 +22,26 @@ macro_rules! conn {
pub type ClientHash = String;
pub type Result<T> = std::result::Result<T, Error>;

/// Errors that can occur when a user votes.
#[derive(Error, Debug)]
pub enum Error {
/// A record could not be created for the user
#[error("failed to create user record")]
FailedToCreateUserRecord,
/// We were unable to delete a user with the given instance ID

#[error("failed to delete user by instance id")]
FailedToDeleteUserRecord,
/// We could not get a vote by a given user

#[error("failed to get user vote")]
FailedToGetUserVote,
/// The user was unable to cast a vote

#[error("failed to cast vote")]
FailedToCastVote,

#[error(transparent)]
Migration(#[from] sqlx::migrate::MigrateError),
/// An error that occurred in category updating

#[error(transparent)]
Sqlx(#[from] sqlx::Error),
/// An error that occurred in the configuration

#[error(transparent)]
Envy(#[from] envy::Error),
}
Expand Down Expand Up @@ -94,10 +94,7 @@ mod tests {
.with_env_filter(EnvFilter::from_default_env())
.init();

let test_users = [
user::User::new(client_hash_1),
user::User::new(client_hash_2),
];
let test_users = [client_hash_1, client_hash_2];

let test_votes = [
vote::Vote {
Expand All @@ -118,29 +115,23 @@ mod tests {

let conn = conn!();

for user in test_users.into_iter() {
user.create_or_seen(conn).await?;
for client_hash in test_users.into_iter() {
User::create_or_seen(client_hash, conn).await?;
}

for vote in test_votes.into_iter() {
vote.save_to_db(conn).await?;
}

let votes_client_1 = vote::Vote::get_all_by_client_hash(
String::from(client_hash_1),
Some(String::from(snap_id_1)),
conn,
)
.await
.unwrap();

let votes_client_2 = vote::Vote::get_all_by_client_hash(
String::from(client_hash_2),
Some(String::from(snap_id_2)),
conn,
)
.await
.unwrap();
let votes_client_1 =
vote::Vote::get_all_by_client_hash(client_hash_1, Some(String::from(snap_id_1)), conn)
.await
.unwrap();

let votes_client_2 =
vote::Vote::get_all_by_client_hash(client_hash_2, Some(String::from(snap_id_2)), conn)
.await
.unwrap();

assert_eq!(votes_client_1.len(), 1);
let first_vote = votes_client_1.first().unwrap();
Expand Down
17 changes: 3 additions & 14 deletions crates/ratings_new/src/db/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,8 @@ pub struct User {
}

impl User {
/// Creates a new user from the given [`ClientHash`]
pub fn new(client_hash: &str) -> Self {
let now = OffsetDateTime::now_utc();
Self {
id: -1,
client_hash: client_hash.to_string(),
last_seen: now,
created: now,
}
}

/// Create a [`User`] entry, or note that the user has recently been seen
pub async fn create_or_seen(self, conn: &mut PgConnection) -> Result<Self> {
pub async fn create_or_seen(client_hash: &str, conn: &mut PgConnection) -> Result<Self> {
let user_with_id = sqlx::query_as(
r#"
INSERT INTO users (client_hash, created, last_seen)
Expand All @@ -38,7 +27,7 @@ impl User {
RETURNING id, client_hash, created, last_seen;
"#,
)
.bind(self.client_hash)
.bind(client_hash)
.fetch_one(conn)
.await
.map_err(|error| {
Expand All @@ -49,7 +38,7 @@ impl User {
Ok(user_with_id)
}

pub async fn delete_by_client_hash(client_hash: String, conn: &mut PgConnection) -> Result<()> {
pub async fn delete_by_client_hash(client_hash: &str, conn: &mut PgConnection) -> Result<()> {
sqlx::query(
r#"
DELETE FROM users
Expand Down
99 changes: 93 additions & 6 deletions crates/ratings_new/src/db/vote.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::{ClientHash, Error, Result};
use sqlx::{types::time::OffsetDateTime, FromRow, PgConnection};
use crate::db::{categories::Category, ClientHash, Error, Result};
use sqlx::{types::time::OffsetDateTime, FromRow, PgConnection, QueryBuilder};
use tracing::error;

/// A Vote, as submitted by a user
Expand All @@ -19,12 +19,12 @@ pub struct Vote {
pub timestamp: OffsetDateTime,
}

/// Gets votes for a snap with the given ID from a given [`ClientHash`]
///
/// [`ClientHash`]: crate::db::ClientHash
impl Vote {
/// Gets votes for a snap with the given ID from a given [`ClientHash`]
///
/// [`ClientHash`]: crate::db::ClientHash
pub async fn get_all_by_client_hash(
client_hash: String,
client_hash: &str,
snap_id_filter: Option<String>,
conn: &mut PgConnection,
) -> Result<Vec<Vote>> {
Expand Down Expand Up @@ -85,3 +85,90 @@ impl Vote {
Ok(result.rows_affected())
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::FromRepr)]
#[repr(i32)]
pub enum Timeframe {
Unspecified,
Week,
Month,
}

/// A summary of votes for a given snap, this is then aggregated before transfer.
#[derive(Debug, Clone, FromRow)]
pub struct VoteSummary {
/// The ID of the snap being checked.
pub snap_id: String,
/// The total votes this snap has received.
pub total_votes: i64,
/// The number of the votes which are positive.
pub positive_votes: i64,
}

impl VoteSummary {
pub async fn get_by_snap_id(snap_id: &str, conn: &mut PgConnection) -> Result<VoteSummary> {
let result: Option<VoteSummary> = sqlx::query_as(
r#"
SELECT
votes.snap_id,
COUNT(*) AS total_votes,
COUNT(*) FILTER (WHERE votes.vote_up) AS positive_votes
FROM
votes
WHERE
votes.snap_id = $1
GROUP BY votes.snap_id
"#,
)
.bind(snap_id)
.fetch_optional(conn)
.await?;

let summary = result.unwrap_or_else(|| VoteSummary {
snap_id: snap_id.to_string(),
total_votes: 0,
positive_votes: 0,
});

Ok(summary)
}

/// Retrieves the vote summary over a given [Timeframe], optionally for a specific [Category]
pub async fn get_for_timeframe(
timeframe: Timeframe,
category: Option<Category>,
conn: &mut PgConnection,
) -> Result<Vec<VoteSummary>> {
let mut builder = QueryBuilder::new(
r"
SELECT
votes.snap_id,
COUNT(*) AS total_votes,
COUNT(*) FILTER (WHERE votes.vote_up) AS positive_votes
FROM
votes",
);

builder.push(match timeframe {
Timeframe::Week => " WHERE votes.created >= NOW() - INTERVAL '1 week'",
Timeframe::Month => " WHERE votes.created >= NOW() - INTERVAL '1 month'",
Timeframe::Unspecified => "",
});

if let Some(category) = category {
builder
.push(
r"
WHERE votes.snap_id IN (
SELECT snap_categories.snap_id FROM snap_categories
WHERE snap_categories.category = $1)",
)
.push_bind(category);
}

builder.push(" GROUP BY votes.snap_id");
let summaries = builder.build_query_as().fetch_all(conn).await?;

Ok(summaries)
}
}
Loading

0 comments on commit 4571f30

Please sign in to comment.