From f7141c5bdeb42e707ed680dd2e28ed1ac82ba352 Mon Sep 17 00:00:00 2001 From: "ayush.jain@juspay.in" Date: Wed, 19 Feb 2025 15:12:18 +0530 Subject: [PATCH] feat: Move experiment API types to superposition_types --- Cargo.lock | 1 + .../src/api/experiments/handlers.rs | 20 +- .../src/api/experiments/types.rs | 228 +--------------- .../tests/experimentation_tests.rs | 9 +- crates/superposition_derives/src/lib.rs | 63 +++++ crates/superposition_types/Cargo.toml | 1 + crates/superposition_types/src/api.rs | 2 + .../src/api/experiments.rs | 247 ++++++++++++++++++ .../superposition_types/src/custom_query.rs | 70 +++-- crates/superposition_types/src/database.rs | 6 + .../src/database/models.rs | 128 ++++++++- .../src/database/models/experimentation.rs | 30 ++- makefile | 2 +- 13 files changed, 520 insertions(+), 287 deletions(-) create mode 100644 crates/superposition_types/src/api/experiments.rs diff --git a/Cargo.lock b/Cargo.lock index 50e8ca6d8..6ef2ac204 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4678,6 +4678,7 @@ dependencies = [ "regex", "serde", "serde_json", + "strum", "strum_macros", "superposition_derives", "thiserror", diff --git a/crates/experimentation_platform/src/api/experiments/handlers.rs b/crates/experimentation_platform/src/api/experiments/handlers.rs index 271d2c900..18435603b 100644 --- a/crates/experimentation_platform/src/api/experiments/handlers.rs +++ b/crates/experimentation_platform/src/api/experiments/handlers.rs @@ -26,6 +26,12 @@ use service_utils::{ }; use superposition_macros::{bad_argument, response_error, unexpected_error}; use superposition_types::{ + api::experiments::{ + ApplicableVariantsQuery, AuditQueryFilters, ConcludeExperimentRequest, + DiscardExperimentRequest, ExperimentCreateRequest, ExperimentCreateResponse, + ExperimentListFilters, ExperimentResponse, ExperimentSortOn, + OverrideKeysUpdateRequest, RampRequest, + }, custom_query::PaginationParams, database::{ models::experimentation::{ @@ -42,18 +48,12 @@ use superposition_types::{ use super::{ helpers::{ add_variant_dimension_to_ctx, check_variant_types, - check_variants_override_coverage, decide_variant, extract_override_keys, - fetch_cac_config, validate_experiment, validate_override_keys, - }, - types::{ - ApplicableVariantsQuery, AuditQueryFilters, ConcludeExperimentRequest, - ContextAction, ContextBulkResponse, ContextMoveReq, ContextPutReq, - DiscardExperimentRequest, ExperimentCreateRequest, ExperimentCreateResponse, - ExperimentListFilters, ExperimentResponse, OverrideKeysUpdateRequest, - RampRequest, + check_variants_override_coverage, construct_header_map, decide_variant, + extract_override_keys, fetch_cac_config, validate_experiment, + validate_override_keys, }, + types::{ContextAction, ContextBulkResponse, ContextMoveReq, ContextPutReq}, }; -use crate::api::experiments::{helpers::construct_header_map, types::ExperimentSortOn}; pub fn endpoints(scope: Scope) -> Scope { scope diff --git a/crates/experimentation_platform/src/api/experiments/types.rs b/crates/experimentation_platform/src/api/experiments/types.rs index ede1ebd3c..88d6e18ea 100644 --- a/crates/experimentation_platform/src/api/experiments/types.rs +++ b/crates/experimentation_platform/src/api/experiments/types.rs @@ -1,124 +1,18 @@ -use std::collections::HashMap; - -use chrono::{DateTime, NaiveDateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use superposition_types::{ - custom_query::{deserialize_stringified_list, CommaSeparatedStringQParams}, - database::models::experimentation::{Experiment, ExperimentStatusType, Variant}, - Condition, Exp, Overrides, SortBy, -}; - -fn default_description() -> String { - String::from("Description not passed") -} - -fn default_change_reason() -> String { - String::from("Change Reason not passed") -} - -#[derive(Deserialize)] -pub struct ExperimentCreateRequest { - pub name: String, - pub context: Exp, - pub variants: Vec, - #[serde(default = "default_description")] - pub description: String, - #[serde(default = "default_change_reason")] - pub change_reason: String, -} - -#[derive(Serialize)] -pub struct ExperimentCreateResponse { - pub experiment_id: String, -} - -impl From for ExperimentCreateResponse { - fn from(experiment: Experiment) -> Self { - Self { - experiment_id: experiment.id.to_string(), - } - } -} - -/********** Experiment Response Type **************/ -// Same as models::Experiments but `id` field is String -// JS have limitation of 53-bit integers, so on -// deserializing from JSON to JS Object will lead incorrect `id` values -#[derive(Serialize, Deserialize)] -pub struct ExperimentResponse { - pub id: String, - pub created_at: DateTime, - pub created_by: String, - pub last_modified: DateTime, - - pub name: String, - pub override_keys: Vec, - pub status: ExperimentStatusType, - pub traffic_percentage: i32, - - pub context: Condition, - pub variants: Vec, - pub last_modified_by: String, - pub chosen_variant: Option, - #[serde(default = "default_description")] - pub description: String, - #[serde(default = "default_change_reason")] - pub change_reason: String, -} - -impl From for ExperimentResponse { - fn from(experiment: Experiment) -> Self { - Self { - id: experiment.id.to_string(), - created_at: experiment.created_at, - created_by: experiment.created_by, - last_modified: experiment.last_modified, - - name: experiment.name, - override_keys: experiment.override_keys, - status: experiment.status, - traffic_percentage: experiment.traffic_percentage, - - context: experiment.context, - variants: experiment.variants.into_inner(), - last_modified_by: experiment.last_modified_by, - chosen_variant: experiment.chosen_variant, - description: experiment.description, - change_reason: experiment.change_reason, - } - } -} - -/********** Experiment Conclude Req Types **********/ - -#[derive(Deserialize, Debug)] -pub struct ConcludeExperimentRequest { - pub chosen_variant: String, - pub description: Option, - #[serde(default = "default_change_reason")] - pub change_reason: String, -} - -/********** Experiment Discard Req Types **********/ - -#[derive(Deserialize, Debug)] -pub struct DiscardExperimentRequest { - #[serde(default = "default_change_reason")] - pub change_reason: String, -} +use superposition_types::database::models::{ChangeReason, Description}; /********** Context Bulk API Type *************/ -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize)] pub struct ContextPutReq { pub context: Map, pub r#override: Value, - pub description: Option, - pub change_reason: String, + pub description: Option, + pub change_reason: ChangeReason, } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize)] pub enum ContextAction { PUT(ContextPutReq), REPLACE(ContextPutReq), @@ -141,117 +35,9 @@ pub enum ContextBulkResponse { MOVE(ContextPutResp), } -/********** Applicable Variants API Type *************/ -#[derive(Debug, Deserialize)] -#[serde(try_from = "HashMap")] -pub struct ApplicableVariantsQuery { - pub context: Map, - pub toss: i8, -} - -impl TryFrom> for ApplicableVariantsQuery { - type Error = String; - fn try_from(value: HashMap) -> Result { - let mut value = value - .into_iter() - .map(|(key, value)| { - (key, value.parse().unwrap_or_else(|_| Value::String(value))) - }) - .collect::>(); - - let toss = value - .remove("toss") - .and_then(|toss| toss.as_i64()) - .and_then(|toss| { - if -1 <= toss && toss <= 100 { - Some(toss as i8) - } else { - None - } - }) - .ok_or_else(|| { - log::error!("toss should be a an interger between -1 and 100 (included)"); - String::from("toss should be a an interger between -1 and 100 (included)") - })?; - - Ok(Self { - toss, - context: value, - }) - } -} - -/********** List API Filter Type *************/ - -#[derive(Deserialize, Debug, Clone)] -pub struct StatusTypes( - #[serde(deserialize_with = "deserialize_stringified_list")] - pub Vec, -); - -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "snake_case")] -pub enum ExperimentSortOn { - LastModifiedAt, - CreatedAt, -} - -impl Default for ExperimentSortOn { - fn default() -> Self { - Self::LastModifiedAt - } -} - -#[derive(Deserialize, Debug)] -pub struct ExperimentListFilters { - pub status: Option, - pub from_date: Option>, - pub to_date: Option>, - pub experiment_name: Option, - pub experiment_ids: Option, - pub created_by: Option, - pub context: Option, - pub sort_on: Option, - pub sort_by: Option, -} - -#[derive(Deserialize, Debug)] -pub struct RampRequest { - pub traffic_percentage: u64, - #[serde(default = "default_change_reason")] - pub change_reason: String, -} - -/********** Update API type ********/ - -#[derive(Deserialize, Debug)] -pub struct VariantUpdateRequest { - pub id: String, - pub overrides: Exp, -} - -#[derive(Deserialize, Debug)] -pub struct OverrideKeysUpdateRequest { - pub variants: Vec, - pub description: Option, - #[serde(default = "default_change_reason")] - pub change_reason: String, -} - #[derive(Deserialize, Serialize, Clone)] pub struct ContextMoveReq { pub context: Map, - pub description: String, - pub change_reason: String, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct AuditQueryFilters { - pub from_date: Option, - pub to_date: Option, - pub table: Option, - pub action: Option, - pub username: Option, - pub count: Option, - pub page: Option, + pub description: Description, + pub change_reason: ChangeReason, } diff --git a/crates/experimentation_platform/tests/experimentation_tests.rs b/crates/experimentation_platform/tests/experimentation_tests.rs index 7dc283ad5..7c8d774a2 100644 --- a/crates/experimentation_platform/tests/experimentation_tests.rs +++ b/crates/experimentation_platform/tests/experimentation_tests.rs @@ -4,8 +4,9 @@ use serde_json::{json, Map, Value}; use service_utils::helpers::extract_dimensions; use service_utils::service::types::ExperimentationFlags; use superposition_types::{ - database::models::experimentation::{ - Experiment, ExperimentStatusType, Variant, Variants, + database::models::{ + experimentation::{Experiment, ExperimentStatusType, Variant, Variants}, + ChangeReason, Description, }, result as superposition, Cac, Condition, Exp, Overrides, }; @@ -73,8 +74,8 @@ fn experiment_gen( context: context.clone(), variants: Variants::new(variants.clone()), chosen_variant: None, - description: "".to_string(), - change_reason: "".to_string(), + description: Description::try_from(String::from("test")).unwrap(), + change_reason: ChangeReason::try_from(String::from("test")).unwrap(), } } diff --git a/crates/superposition_derives/src/lib.rs b/crates/superposition_derives/src/lib.rs index 36302a9f8..c341d81dc 100644 --- a/crates/superposition_derives/src/lib.rs +++ b/crates/superposition_derives/src/lib.rs @@ -46,3 +46,66 @@ pub fn json_to_sql_derive(input: TokenStream) -> TokenStream { TokenStream::from(expanded) } + +/// Implements `FromSql` trait for converting `Text` type to the type for `Pg` backend +/// +#[proc_macro_derive(TextFromSql)] +pub fn text_from_sql_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = input.ident; + + let expanded = quote! { + impl diesel::deserialize::FromSql for #name { + fn from_sql(bytes: diesel::pg::PgValue<'_>) -> diesel::deserialize::Result { + let text = >::from_sql(bytes)?; + text.try_into().map_err(|e: String| Box::::from(e)) + } + } + }; + + TokenStream::from(expanded) +} + +/// Implements `ToSql` trait for converting the typed data to `Json` type for `Pg` backend +/// +#[proc_macro_derive(TextToSql)] +pub fn text_to_sql_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = input.ident; + + let expanded = quote! { + impl diesel::serialize::ToSql for #name { + fn to_sql<'b>( + &'b self, + out: &mut diesel::serialize::Output<'b, '_, diesel::pg::Pg>, + ) -> diesel::serialize::Result { + let text: String = self.into(); + >::to_sql(&text, &mut out.reborrow()) + } + } + }; + + TokenStream::from(expanded) +} + +/// Implements `FromSql` trait for converting `Text` type to the type for `Pg` backend +/// +#[proc_macro_derive(TextFromSqlNoValidation)] +pub fn text_from_sql_derive_no_validation(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = input.ident; + + let expanded = quote! { + impl diesel::deserialize::FromSql for #name { + fn from_sql(bytes: diesel::pg::PgValue<'_>) -> diesel::deserialize::Result { + let text = >::from_sql(bytes)?; + Ok(<#name as DisableDBValidation>::from_db_unvalidated(text)) + } + } + }; + + TokenStream::from(expanded) +} diff --git a/crates/superposition_types/Cargo.toml b/crates/superposition_types/Cargo.toml index 57abdfc6c..18e453e68 100644 --- a/crates/superposition_types/Cargo.toml +++ b/crates/superposition_types/Cargo.toml @@ -21,6 +21,7 @@ log = { workspace = true } regex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +strum = { workspace = true } strum_macros = { workspace = true } superposition_derives = { path = "../superposition_derives", optional = true } thiserror = { version = "1.0.57", optional = true } diff --git a/crates/superposition_types/src/api.rs b/crates/superposition_types/src/api.rs index b91220bb3..405f11df7 100644 --- a/crates/superposition_types/src/api.rs +++ b/crates/superposition_types/src/api.rs @@ -1 +1,3 @@ +#[cfg(feature = "experimentation")] +pub mod experiments; pub mod workspace; diff --git a/crates/superposition_types/src/api/experiments.rs b/crates/superposition_types/src/api/experiments.rs new file mode 100644 index 000000000..1d2caa37f --- /dev/null +++ b/crates/superposition_types/src/api/experiments.rs @@ -0,0 +1,247 @@ +use std::{collections::HashMap, fmt::Display}; + +use chrono::{DateTime, NaiveDateTime, Utc}; +use core::fmt; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use strum_macros::Display; + +use crate::{ + custom_query::{CommaSeparatedQParams, CommaSeparatedStringQParams}, + database::models::{ + experimentation::{Experiment, ExperimentStatusType, Variant, Variants}, + ChangeReason, Description, + }, + Condition, Exp, Overrides, SortBy, +}; + +/********** Experiment Response Type **************/ +// Same as models::Experiments but `id` field is String +// JS have limitation of 53-bit integers, so on +// deserializing from JSON to JS Object will lead incorrect `id` values +#[repr(C)] +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ExperimentResponse { + pub id: String, + pub created_at: DateTime, + pub created_by: String, + pub last_modified: DateTime, + + pub name: String, + pub override_keys: Vec, + pub status: ExperimentStatusType, + pub traffic_percentage: u8, + + pub context: Condition, + pub variants: Variants, + pub last_modified_by: String, + pub chosen_variant: Option, + pub description: Description, + pub change_reason: ChangeReason, +} + +impl From for ExperimentResponse { + fn from(experiment: Experiment) -> Self { + Self { + id: experiment.id.to_string(), + created_at: experiment.created_at, + created_by: experiment.created_by, + last_modified: experiment.last_modified, + + name: experiment.name, + override_keys: experiment.override_keys, + status: experiment.status, + traffic_percentage: experiment.traffic_percentage as u8, + + context: experiment.context, + variants: experiment.variants, + last_modified_by: experiment.last_modified_by, + chosen_variant: experiment.chosen_variant, + description: experiment.description, + change_reason: experiment.change_reason, + } + } +} + +#[derive(Deserialize, Serialize)] +pub struct ExperimentCreateRequest { + pub name: String, + pub context: Exp, + pub variants: Vec, + #[serde(default = "Description::default")] + pub description: Description, + #[serde(default = "ChangeReason::default")] + pub change_reason: ChangeReason, +} + +#[derive(Deserialize, Serialize)] +pub struct ExperimentCreateResponse { + pub experiment_id: String, +} + +impl From for ExperimentCreateResponse { + fn from(experiment: Experiment) -> Self { + Self { + experiment_id: experiment.id.to_string(), + } + } +} + +/********** Experiment Ramp Req Types **********/ + +#[derive(Deserialize, Serialize, Debug)] +pub struct RampRequest { + pub traffic_percentage: u64, + #[serde(default = "ChangeReason::default")] + pub change_reason: ChangeReason, +} + +/********** Experiment Conclude Req Types **********/ + +#[derive(Deserialize, Serialize, Debug)] +pub struct ConcludeExperimentRequest { + pub chosen_variant: String, + pub description: Option, + #[serde(default = "ChangeReason::default")] + pub change_reason: ChangeReason, +} + +/********** Experiment Discard Req Types **********/ + +#[derive(Deserialize, Serialize, Debug)] +pub struct DiscardExperimentRequest { + #[serde(default = "ChangeReason::default")] + pub change_reason: ChangeReason, +} + +/********** Applicable Variants API Type *************/ +#[derive(Debug, Deserialize)] +#[serde(try_from = "HashMap")] +pub struct ApplicableVariantsQuery { + pub context: Map, + pub toss: i8, +} + +impl TryFrom> for ApplicableVariantsQuery { + type Error = String; + fn try_from(value: HashMap) -> Result { + let mut value = value + .into_iter() + .map(|(key, value)| { + (key, value.parse().unwrap_or_else(|_| Value::String(value))) + }) + .collect::>(); + + let toss = value + .remove("toss") + .and_then(|toss| toss.as_i64()) + .and_then(|toss| { + if -1 <= toss && toss <= 100 { + Some(toss as i8) + } else { + None + } + }) + .ok_or_else(|| { + log::error!("toss should be a an interger between -1 and 100 (included)"); + String::from("toss should be a an interger between -1 and 100 (included)") + })?; + + Ok(Self { + toss, + context: value, + }) + } +} + +/********** List API Filter Type *************/ + +#[derive(Copy, Display, Deserialize, Serialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum ExperimentSortOn { + LastModifiedAt, + CreatedAt, +} + +impl Default for ExperimentSortOn { + fn default() -> Self { + Self::LastModifiedAt + } +} + +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +pub struct ExperimentListFilters { + pub status: Option>, + pub from_date: Option>, + pub to_date: Option>, + pub experiment_name: Option, + pub experiment_ids: Option, + pub created_by: Option, + pub context: Option, + pub sort_on: Option, + pub sort_by: Option, +} + +impl Display for ExperimentListFilters { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut query_params = vec![]; + if let Some(status) = &self.status { + let status: Vec = + status.0.iter().map(|val| val.to_string()).collect(); + query_params.push(format!("status={}", status.join(","))); + } + if let Some(from_date) = self.from_date { + query_params.push(format!("from_date={}", from_date)); + } + if let Some(to_date) = self.to_date { + query_params.push(format!("to_date={}", to_date)); + } + if let Some(experiment_name) = &self.experiment_name { + query_params.push(format!("experiment_name={}", experiment_name)); + } + if let Some(experiment_ids) = &self.experiment_ids { + query_params.push(format!("experiment_ids={}", experiment_ids)); + } + if let Some(created_by) = &self.created_by { + query_params.push(format!("created_by={}", created_by)); + } + if let Some(context) = &self.context { + query_params.push(format!("context={}", context)); + } + if let Some(sort_on) = self.sort_on { + query_params.push(format!("sort_on={}", sort_on)); + } + if let Some(sort_by) = &self.sort_by { + query_params.push(format!("sort_by={}", sort_by)); + } + write!(f, "{}", query_params.join("&")) + } +} + +/********** Update API type ********/ + +#[derive(Deserialize, Serialize, Debug)] +pub struct VariantUpdateRequest { + pub id: String, + pub overrides: Exp, +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct OverrideKeysUpdateRequest { + pub variants: Vec, + pub description: Option, + #[serde(default = "ChangeReason::default")] + pub change_reason: ChangeReason, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AuditQueryFilters { + pub from_date: Option, + pub to_date: Option, + pub table: Option, + pub action: Option, + pub username: Option, + pub count: Option, + pub page: Option, +} diff --git a/crates/superposition_types/src/custom_query.rs b/crates/superposition_types/src/custom_query.rs index c73a0d340..28eafbdbf 100644 --- a/crates/superposition_types/src/custom_query.rs +++ b/crates/superposition_types/src/custom_query.rs @@ -1,11 +1,11 @@ -use std::{collections::HashMap, fmt::Display}; +use std::{collections::HashMap, fmt::Display, str::FromStr}; use core::fmt; use derive_more::{Deref, DerefMut}; use regex::Regex; use serde::{ - de::{self, DeserializeOwned, IntoDeserializer}, - Deserialize, Deserializer, + de::{self, DeserializeOwned}, + Deserialize, Deserializer, Serialize, }; use serde_json::{Map, Value}; @@ -243,49 +243,45 @@ impl<'de> Deserialize<'de> for PaginationParams { } } -#[derive(Debug, Deserialize, Clone, Deref)] +#[derive(Debug, Clone, Deref, PartialEq)] #[deref(forward)] -pub struct CommaSeparatedStringQParams( - #[serde(deserialize_with = "deserialize_stringified_list")] pub Vec, -); +pub struct CommaSeparatedQParams(pub Vec); -pub fn deserialize_stringified_list<'de, D, I>( - deserializer: D, -) -> std::result::Result, D::Error> -where - D: de::Deserializer<'de>, - I: de::DeserializeOwned, -{ - struct StringVecVisitor(std::marker::PhantomData); +impl Display for CommaSeparatedQParams { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str_arr = self.0.iter().map(|s| s.to_string()).collect::>(); + write!(f, "{}", str_arr.join(",")) + } +} - impl<'de, I> de::Visitor<'de> for StringVecVisitor +impl<'de, T: Display + FromStr> Deserialize<'de> for CommaSeparatedQParams { + fn deserialize(deserializer: D) -> Result where - I: de::DeserializeOwned, + D: Deserializer<'de>, { - type Value = Vec; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str( - "a string containing comma separated values eg: CREATED,INPROGRESS", - ) - } - - fn visit_str(self, v: &str) -> std::result::Result - where - E: de::Error, - { - let mut query_vector = Vec::new(); - for param in v.split(',') { - let p: I = I::deserialize(param.into_deserializer())?; - query_vector.push(p); - } - Ok(query_vector) - } + let items = String::deserialize(deserializer)? + .split(',') + .map(|item| item.trim().to_string()) + .map(|s| T::from_str(&s)) + .collect::, _>>() + .map_err(|_| { + serde::de::Error::custom(String::from("Error in converting type")) + })?; + Ok(Self(items)) } +} - deserializer.deserialize_any(StringVecVisitor(std::marker::PhantomData::)) +impl Serialize for CommaSeparatedQParams { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } } +pub type CommaSeparatedStringQParams = CommaSeparatedQParams; + #[cfg(test)] mod tests { use std::collections::HashMap; diff --git a/crates/superposition_types/src/database.rs b/crates/superposition_types/src/database.rs index 95befe82f..180a1ca13 100644 --- a/crates/superposition_types/src/database.rs +++ b/crates/superposition_types/src/database.rs @@ -4,3 +4,9 @@ pub mod schema; #[cfg(feature = "diesel_derives")] pub mod superposition_schema; pub mod types; + +#[cfg(feature = "disable_db_data_validation")] +pub trait DisableDBValidation { + type From; + fn from_db_unvalidated(data: Self::From) -> Self; +} diff --git a/crates/superposition_types/src/database/models.rs b/crates/superposition_types/src/database/models.rs index dbf1ddc71..0183f8b7c 100644 --- a/crates/superposition_types/src/database/models.rs +++ b/crates/superposition_types/src/database/models.rs @@ -1,16 +1,130 @@ -use chrono::NaiveDateTime; -#[cfg(feature = "diesel_derives")] -use diesel::{AsChangeset, Insertable, QueryId, Queryable, Selectable}; -use std::str::FromStr; - -use serde::{Deserialize, Serialize}; - pub mod cac; #[cfg(feature = "experimentation")] pub mod experimentation; +use std::str::FromStr; + +use chrono::NaiveDateTime; +#[cfg(feature = "diesel_derives")] +use diesel::{ + sql_types::Text, AsChangeset, AsExpression, FromSqlRow, Insertable, QueryId, + Queryable, Selectable, +}; +use serde::{Deserialize, Serialize}; +#[cfg(all( + feature = "diesel_derives", + not(feature = "disable_db_data_validation") +))] +use superposition_derives::TextFromSql; +#[cfg(all(feature = "diesel_derives", feature = "disable_db_data_validation"))] +use superposition_derives::TextFromSqlNoValidation; +#[cfg(feature = "diesel_derives")] +use superposition_derives::TextToSql; + #[cfg(feature = "diesel_derives")] use super::superposition_schema::superposition::*; +#[cfg(feature = "disable_db_data_validation")] +use super::DisableDBValidation; + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(try_from = "String")] +#[cfg_attr( + feature = "diesel_derives", + derive(AsExpression, FromSqlRow, TextToSql) +)] +#[cfg_attr( + all( + feature = "diesel_derives", + not(feature = "disable_db_data_validation") + ), + derive(TextFromSql) +)] +#[cfg_attr( + all(feature = "diesel_derives", feature = "disable_db_data_validation"), + derive(TextFromSqlNoValidation) +)] +#[cfg_attr(feature = "diesel_derives", diesel(sql_type = Text))] +pub struct ChangeReason(String); + +impl Default for ChangeReason { + fn default() -> Self { + Self(String::from("Change Reason not provided")) + } +} + +#[cfg(feature = "disable_db_data_validation")] +impl DisableDBValidation for ChangeReason { + type From = String; + fn from_db_unvalidated(data: Self::From) -> Self { + Self::try_from(data).unwrap_or_default() + } +} + +impl From<&ChangeReason> for String { + fn from(value: &ChangeReason) -> String { + value.0.clone() + } +} + +impl TryFrom for ChangeReason { + type Error = String; + fn try_from(value: String) -> Result { + if value.len() == 0 { + return Err(String::from("Empty reason not allowed")); + } + Ok(Self(value)) + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(try_from = "String")] +#[cfg_attr( + feature = "diesel_derives", + derive(AsExpression, FromSqlRow, TextToSql) +)] +#[cfg_attr( + all( + feature = "diesel_derives", + not(feature = "disable_db_data_validation") + ), + derive(TextFromSql) +)] +#[cfg_attr( + all(feature = "diesel_derives", feature = "disable_db_data_validation"), + derive(TextFromSqlNoValidation) +)] +#[cfg_attr(feature = "diesel_derives", diesel(sql_type = Text))] +pub struct Description(String); + +impl Default for Description { + fn default() -> Self { + Self(String::from("Description not provided")) + } +} + +#[cfg(feature = "disable_db_data_validation")] +impl DisableDBValidation for Description { + type From = String; + fn from_db_unvalidated(data: Self::From) -> Self { + Self::try_from(data).unwrap_or_default() + } +} + +impl From<&Description> for String { + fn from(value: &Description) -> String { + value.0.clone() + } +} + +impl TryFrom for Description { + type Error = String; + fn try_from(value: String) -> Result { + if value.len() == 0 { + return Err(String::from("Empty description not allowed")); + } + Ok(Self(value)) + } +} #[derive( Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display, diff --git a/crates/superposition_types/src/database/models/experimentation.rs b/crates/superposition_types/src/database/models/experimentation.rs index a23c554e7..e58ddfb4c 100644 --- a/crates/superposition_types/src/database/models/experimentation.rs +++ b/crates/superposition_types/src/database/models/experimentation.rs @@ -1,4 +1,5 @@ use chrono::{DateTime, NaiveDateTime, Utc}; +use derive_more::{Deref, DerefMut}; #[cfg(feature = "diesel_derives")] use diesel::{ deserialize::FromSqlRow, expression::AsExpression, sql_types::Json, Insertable, @@ -9,13 +10,21 @@ use serde_json::Value; #[cfg(feature = "diesel_derives")] use superposition_derives::{JsonFromSql, JsonToSql}; -use crate::{Condition, Exp, Overrides}; +use crate::{Condition, Exp, Overridden, Overrides}; #[cfg(feature = "diesel_derives")] use super::super::schema::*; +use super::{ChangeReason, Description}; #[derive( - Debug, Clone, Copy, PartialEq, Deserialize, Serialize, strum_macros::Display, + Debug, + Clone, + Copy, + PartialEq, + Deserialize, + Serialize, + strum_macros::Display, + strum_macros::EnumString, )] #[serde(rename_all = "UPPERCASE")] #[strum(serialize_all = "UPPERCASE")] @@ -58,6 +67,7 @@ pub enum VariantType { EXPERIMENTAL, } +#[repr(C)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Variant { pub id: String, @@ -69,10 +79,16 @@ pub struct Variant { pub overrides: Exp, } -#[derive(Debug, Clone, Serialize, Deserialize)] +impl Overridden> for Variant { + fn get_overrides(&self) -> Overrides { + self.overrides.clone().into_inner() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Deref, DerefMut)] #[cfg_attr( feature = "diesel_derives", - derive(AsExpression, FromSqlRow, JsonFromSql, JsonToSql,) + derive(AsExpression, FromSqlRow, JsonFromSql, JsonToSql) )] #[cfg_attr(feature = "diesel_derives", diesel(sql_type = Json))] pub struct Variants(Vec); @@ -87,7 +103,7 @@ impl Variants { } } -#[derive(Serialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone)] #[cfg_attr( feature = "diesel_derives", derive(QueryableByName, Queryable, Selectable, Insertable) @@ -109,8 +125,8 @@ pub struct Experiment { pub variants: Variants, pub last_modified_by: String, pub chosen_variant: Option, - pub description: String, - pub change_reason: String, + pub description: Description, + pub change_reason: ChangeReason, } pub type Experiments = Vec; diff --git a/makefile b/makefile index 683112fc5..c855eb5a7 100644 --- a/makefile +++ b/makefile @@ -153,7 +153,7 @@ run: kill db localstack frontend superposition @./target/debug/superposition run_legacy: kill build db localstack superposition_legacy - @./target/debug/superposition_legacy + @./target/debug/superposition test: WASM_PACK_MODE=--profiling test: setup frontend superposition