Skip to content

Commit

Permalink
feat: Move experiment API types to superposition_types
Browse files Browse the repository at this point in the history
  • Loading branch information
ayushjain17 committed Feb 19, 2025
1 parent 2cb6840 commit f7141c5
Show file tree
Hide file tree
Showing 13 changed files with 520 additions and 287 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

20 changes: 10 additions & 10 deletions crates/experimentation_platform/src/api/experiments/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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
Expand Down
228 changes: 7 additions & 221 deletions crates/experimentation_platform/src/api/experiments/types.rs
Original file line number Diff line number Diff line change
@@ -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<Condition>,
pub variants: Vec<Variant>,
#[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<Experiment> 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<Utc>,
pub created_by: String,
pub last_modified: DateTime<Utc>,

pub name: String,
pub override_keys: Vec<String>,
pub status: ExperimentStatusType,
pub traffic_percentage: i32,

pub context: Condition,
pub variants: Vec<Variant>,
pub last_modified_by: String,
pub chosen_variant: Option<String>,
#[serde(default = "default_description")]
pub description: String,
#[serde(default = "default_change_reason")]
pub change_reason: String,
}

impl From<Experiment> 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<String>,
#[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<String, Value>,
pub r#override: Value,
pub description: Option<String>,
pub change_reason: String,
pub description: Option<Description>,
pub change_reason: ChangeReason,
}

#[derive(Deserialize, Serialize, Clone)]
#[derive(Deserialize, Serialize)]
pub enum ContextAction {
PUT(ContextPutReq),
REPLACE(ContextPutReq),
Expand All @@ -141,117 +35,9 @@ pub enum ContextBulkResponse {
MOVE(ContextPutResp),
}

/********** Applicable Variants API Type *************/
#[derive(Debug, Deserialize)]
#[serde(try_from = "HashMap<String,String>")]
pub struct ApplicableVariantsQuery {
pub context: Map<String, Value>,
pub toss: i8,
}

impl TryFrom<HashMap<String, String>> for ApplicableVariantsQuery {
type Error = String;
fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
let mut value = value
.into_iter()
.map(|(key, value)| {
(key, value.parse().unwrap_or_else(|_| Value::String(value)))
})
.collect::<Map<_, _>>();

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<ExperimentStatusType>,
);

#[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<StatusTypes>,
pub from_date: Option<DateTime<Utc>>,
pub to_date: Option<DateTime<Utc>>,
pub experiment_name: Option<String>,
pub experiment_ids: Option<CommaSeparatedStringQParams>,
pub created_by: Option<CommaSeparatedStringQParams>,
pub context: Option<String>,
pub sort_on: Option<ExperimentSortOn>,
pub sort_by: Option<SortBy>,
}

#[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<Overrides>,
}

#[derive(Deserialize, Debug)]
pub struct OverrideKeysUpdateRequest {
pub variants: Vec<VariantUpdateRequest>,
pub description: Option<String>,
#[serde(default = "default_change_reason")]
pub change_reason: String,
}

#[derive(Deserialize, Serialize, Clone)]
pub struct ContextMoveReq {
pub context: Map<String, Value>,
pub description: String,
pub change_reason: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct AuditQueryFilters {
pub from_date: Option<NaiveDateTime>,
pub to_date: Option<NaiveDateTime>,
pub table: Option<CommaSeparatedStringQParams>,
pub action: Option<CommaSeparatedStringQParams>,
pub username: Option<String>,
pub count: Option<i64>,
pub page: Option<i64>,
pub description: Description,
pub change_reason: ChangeReason,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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(),
}
}

Expand Down
63 changes: 63 additions & 0 deletions crates/superposition_derives/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<diesel::sql_types::Text, diesel::pg::Pg> for #name {
fn from_sql(bytes: diesel::pg::PgValue<'_>) -> diesel::deserialize::Result<Self> {
let text = <String as diesel::deserialize::FromSql<diesel::sql_types::Text, diesel::pg::Pg>>::from_sql(bytes)?;
text.try_into().map_err(|e: String| Box::<dyn std::error::Error + Send + Sync>::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<diesel::sql_types::Text, diesel::pg::Pg> 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();
<String as diesel::serialize::ToSql<
diesel::sql_types::Text,
diesel::pg::Pg,
>>::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<diesel::sql_types::Text, diesel::pg::Pg> for #name {
fn from_sql(bytes: diesel::pg::PgValue<'_>) -> diesel::deserialize::Result<Self> {
let text = <String as diesel::deserialize::FromSql<diesel::sql_types::Text, diesel::pg::Pg>>::from_sql(bytes)?;
Ok(<#name as DisableDBValidation>::from_db_unvalidated(text))
}
}
};

TokenStream::from(expanded)
}
1 change: 1 addition & 0 deletions crates/superposition_types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
2 changes: 2 additions & 0 deletions crates/superposition_types/src/api.rs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#[cfg(feature = "experimentation")]
pub mod experiments;
pub mod workspace;
Loading

0 comments on commit f7141c5

Please sign in to comment.