Rating type instead of bool, upsert query, enums

This commit is contained in:
Brandon Thomas committed 2023-01-25 02:33:56 -05:00
1 parent bcbb36a9ed
commit 1bb0dda330
10 files changed
+332 -1

No files matched your search

+1
View File
@@ -6,6 +6,7 @@
<w>cashapp</w>
<w>cheermote</w>
<w>crockford</w>
<w>downvote</w>
<w>fakeyou</w>
<w>ffprobe</w>
<w>hifigan</w>
@@ -20,6 +20,14 @@ CREATE TABLE user_ratings (
-- Whether the vote is up/positive (TRUE) or down/negative (FALSE)
is_positive_rating BOOLEAN NOT NULL DEFAULT FALSE,
-- Whether the vote is "soft deleted" (="neutral"), up/positive, or down/negative.
-- Rather than a nullable bool for ratings, we'll provide an enum.
rating_type ENUM(
'neutral',
'positive',
'negative'
) NOT NULL DEFAULT 'neutral',
-- For abuse tracking.
-- Wide enough for IPv4/6
vote_ip_address VARCHAR(40) NOT NULL,
@@ -38,6 +46,7 @@ CREATE TABLE user_ratings (
PRIMARY KEY (id),
UNIQUE KEY (user_token, entity_type, entity_token),
KEY index_entity_type_entity_token (entity_type, entity_token),
KEY index_user_token (user_token)
KEY index_user_token (user_token),
KEY index_rating_type (rating_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
+1
View File
@@ -1,3 +1,4 @@
pub mod media_uploads;
pub mod model_categories;
pub mod trending_model_analytics;
pub mod user_ratings;
@@ -0,0 +1,93 @@
use serde::Deserialize;
use serde::Serialize;
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
use strum::EnumIter;
/// Used in the `user_ratings` table in a `VARCHAR(32)` field named `entity_type`.
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub enum UserRatingEntityType {
/// TTS model (architecture does not matter)
#[serde(rename = "tts_model")]
TtsModel,
/// TTS result (architecture does not matter)
#[serde(rename = "tts_result")]
TtsResult,
/// W2L template
#[serde(rename = "w2l_template")]
W2lTemplate,
/// W2L result
#[serde(rename = "w2l_result")]
W2lResult,
}
// TODO(bt, 2023-01-17): This desperately needs MySQL integration tests!
impl_enum_display_and_debug_using_to_str!(UserRatingEntityType);
impl_enum_sqlx_coders!(UserRatingEntityType);
/// NB: Legacy API for older code.
impl UserRatingEntityType {
pub fn to_str(&self) -> &'static str {
match self {
Self::TtsModel => "tts_model",
Self::TtsResult => "tts_result",
Self::W2lTemplate => "w2l_template",
Self::W2lResult => "w2l_result",
}
}
pub fn from_str(value: &str) -> Result<Self, String> {
match value {
"tts_model" => Ok(Self::TtsModel),
"tts_result" => Ok(Self::TtsResult),
"w2l_template" => Ok(Self::W2lTemplate),
"w2l_result" => Ok(Self::W2lResult),
_ => Err(format!("invalid value: {:?}", value)),
}
}
}
#[cfg(test)]
mod tests {
use crate::by_table::user_ratings::entity_type::UserRatingEntityType;
use crate::test_helpers::assert_serialization;
mod serde {
use super::*;
#[test]
fn test_serialization() {
assert_serialization(UserRatingEntityType::TtsModel, "tts_model");
assert_serialization(UserRatingEntityType::TtsResult, "tts_result");
assert_serialization(UserRatingEntityType::W2lTemplate, "w2l_template");
assert_serialization(UserRatingEntityType::W2lResult, "w2l_result");
}
}
mod impl_methods {
use super::*;
#[test]
fn test_to_str() {
assert_eq!(UserRatingEntityType::TtsModel.to_str(), "tts_model");
assert_eq!(UserRatingEntityType::TtsResult.to_str(), "tts_result");
assert_eq!(UserRatingEntityType::W2lTemplate.to_str(), "w2l_template");
assert_eq!(UserRatingEntityType::W2lResult.to_str(), "w2l_result");
}
#[test]
fn test_from_str() {
assert_eq!(UserRatingEntityType::from_str("tts_model").unwrap(), UserRatingEntityType::TtsModel);
assert_eq!(UserRatingEntityType::from_str("tts_result").unwrap(), UserRatingEntityType::TtsResult);
assert_eq!(UserRatingEntityType::from_str("w2l_template").unwrap(), UserRatingEntityType::W2lTemplate);
assert_eq!(UserRatingEntityType::from_str("w2l_result").unwrap(), UserRatingEntityType::W2lResult);
assert!(UserRatingEntityType::from_str("foo").is_err());
}
}
}
@@ -0,0 +1,2 @@
pub mod entity_type;
pub mod rating_type;
@@ -0,0 +1,161 @@
#[cfg(test)]
use strum::EnumCount;
#[cfg(test)]
use strum::EnumIter;
/// UserRatingType
///
/// - Used in the `user_ratings` table as an `ENUM` field named `rating_type`.
/// - Used in the HTTP API.
///
/// To use this in a query, the query must have type annotations.
/// See: https://www.gitmemory.com/issue/launchbadge/sqlx/1241/847154375
/// eg. preferred_tts_result_visibility as `rating_type: enums::by_table::user_ratings::rating_type::UserRatingType`
///
/// See also: https://docs.rs/sqlx/0.4.0-beta.1/sqlx/trait.Type.html
///
/// *DO NOT CHANGE VALUES WITHOUT A MIGRATION STRATEGY!*
///
#[cfg_attr(test, derive(EnumIter, EnumCount))]
#[derive(Clone, Copy, Eq, PartialEq, Deserialize, Serialize, sqlx::Type)]
#[serde(rename_all = "lowercase")]
#[sqlx(rename_all = "lowercase")]
pub enum UserRatingType {
/// This is considered a ratings "soft deletion" and does not count towards a total score.
/// This is the default rating.
Neutral,
/// This is a positive vote / upvote / like.
/// They are available to non-logged-in users as long as they have the URL.
Positive,
/// This is a negative vote / downvote / dislike.
Negative,
}
impl_enum_display_and_debug_using_to_str!(UserRatingType);
impl Default for UserRatingType {
fn default() -> Self { Self::Neutral }
}
/// NB: Legacy API for older code.
impl UserRatingType {
pub fn to_str(&self) -> &'static str {
match self {
Self::Neutral => "neutral",
Self::Positive => "positive",
Self::Negative => "negative",
}
}
pub fn from_str(value: &str) -> Result<Self, String> {
match value {
"neutral" => Ok(Self::Neutral),
"positive" => Ok(Self::Positive),
"negative" => Ok(Self::Negative),
_ => Err(format!("invalid value: {:?}", value)),
}
}
}
#[cfg(test)]
mod tests {
use crate::by_table::user_ratings::rating_type::UserRatingType;
use crate::test_helpers::assert_serialization;
#[test]
fn test_serialization() {
assert_serialization(UserRatingType::Neutral, "neutral");
assert_serialization(UserRatingType::Positive, "positive");
assert_serialization(UserRatingType::Negative, "negative");
}
mod impl_methods {
use super::*;
#[test]
fn to_str() {
assert_eq!(UserRatingType::Neutral.to_str(), "neutral");
assert_eq!(UserRatingType::Positive.to_str(), "positive");
assert_eq!(UserRatingType::Negative.to_str(), "negative");
}
#[test]
fn from_str() {
assert_eq!(UserRatingType::from_str("neutral").unwrap(), UserRatingType::Neutral);
assert_eq!(UserRatingType::from_str("positive").unwrap(), UserRatingType::Positive);
assert_eq!(UserRatingType::from_str("negative").unwrap(), UserRatingType::Negative);
assert!(UserRatingType::from_str("foo").is_err());
}
}
mod traits {
use super::*;
#[test]
fn default() {
assert_eq!(UserRatingType::default(), UserRatingType::Neutral);
}
#[test]
fn display() {
let visibility = UserRatingType::Positive;
assert_eq!(format!("{}", visibility), "positive".to_string());
}
#[test]
fn debug() {
let visibility = UserRatingType::Negative;
assert_eq!(format!("{:?}", visibility), "negative".to_string());
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct CompositeType {
visibility: UserRatingType,
string: String,
}
mod serde_serialization {
use super::*;
#[test]
fn serialize() {
let expected = "\"positive\"".to_string(); // NB: Quoted
assert_eq!(expected, toml::to_string(&UserRatingType::Positive).unwrap());
// Just to show this serializes the same as a string
assert_eq!(expected, toml::to_string("positive").unwrap());
}
#[test]
fn nested_serialize() {
let value = CompositeType { visibility: UserRatingType::Negative, string: "bar".to_string() };
let expected = r#"{"visibility":"negative","string":"bar"}"#.to_string();
assert_eq!(expected, serde_json::to_string(&value).unwrap());
}
}
mod serde_deserialization {
use super::*;
#[test]
fn deserialize() {
let payload = "\"positive\""; // NB: Quoted
let value: UserRatingType = serde_json::from_str(payload).unwrap();
assert_eq!(value, UserRatingType::Positive);
}
#[test]
fn nested_deserialize() {
let payload = r#"{"visibility":"neutral","string":"bar"}"#.to_string();
let expected = CompositeType {
visibility: UserRatingType::Neutral,
string: "bar".to_string(),
};
assert_eq!(expected, serde_json::from_str::<CompositeType>(&payload).unwrap());
}
}
}
+4
View File
@@ -4,6 +4,10 @@
//! This should also work for CHAR/VARCHAR fields that work similarly to enums (typically
//! as part of a composite key)
//!
//! These types should also be friendly for API usage in JSON payloads.
//!
//! In the future this should be *CODEGEN DRIVEN* and should get checked into source control.
//!
// Never allow these
#![forbid(private_in_public)]
@@ -1,6 +1,7 @@
pub mod user;
pub mod user_badges;
pub mod user_profiles;
pub mod user_ratings;
pub mod user_roles;
pub mod user_sessions;
pub mod user_subscriptions;
@@ -0,0 +1 @@
pub mod upsert_user_rating;
@@ -0,0 +1,58 @@
use container_common::anyhow_result::AnyhowResult;
use enums::by_table::user_ratings::entity_type::UserRatingEntityType;
use sqlx::MySql;
use sqlx::pool::PoolConnection;
use enums::by_table::user_ratings::rating_type::UserRatingType;
use tokens::tokens::tts_models::TtsModelToken;
use tokens::tokens::w2l_templates::W2lTemplateToken;
use tokens::users::user::UserToken;
pub enum UserRatingEntityToken<'a> {
TtsModel(&'a TtsModelToken),
W2lTemplate(&'a W2lTemplateToken),
}
pub struct Args<'a> {
pub user_token: &'a UserToken,
pub entity_token: UserRatingEntityToken<'a>,
pub rating_type: UserRatingType,
pub ip_address: &'a str,
pub mysql_connection: &'a mut PoolConnection<MySql>,
}
pub async fn upsert_user_rating(args: Args<'_>) -> AnyhowResult<()> {
let (entity_type, entity_token) = match args.entity_token {
EntityToken::TtsModel(token) => (UserRatingEntityType::TtsModel, token.as_str()),
EntityToken::W2lTemplate(token) => (UserRatingEntityType::W2lTemplate, token.as_str()),
};
let query = sqlx::query!(
r#"
INSERT INTO user_ratings
SET
user_token = ?,
entity_type = ?,
entity_token = ?,
rating_type = ?,
vote_ip_address = ?,
version = 1
ON DUPLICATE KEY UPDATE
rating_type = ?,
vote_ip_address = ?,
version = version + 1
"#,
// Insert
args.user_token.as_str(),
entity_type,
entity_token,
args.rating_type.to_str(),
args.ip_address,
args.rating_type.to_str(),
args.ip_address
);
let _r = query.execute(args.mysql_connection).await?;
Ok(())
}