use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use stateset_primitives::{CustomerId, SegmentId};
use strum::{Display, EnumString};
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Display, EnumString,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[non_exhaustive]
pub enum SegmentType {
#[default]
Static,
Dynamic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
#[non_exhaustive]
pub enum SegmentOperator {
Eq,
Neq,
Gt,
Gte,
Lt,
Lte,
Contains,
In,
Between,
StartsWith,
EndsWith,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SegmentRule {
pub field: String,
pub operator: SegmentOperator,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Segment {
pub id: SegmentId,
pub name: String,
pub description: Option<String>,
pub segment_type: SegmentType,
pub rules: Vec<SegmentRule>,
pub member_count: u64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SegmentMembership {
pub segment_id: SegmentId,
pub customer_id: CustomerId,
pub joined_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateSegment {
pub name: String,
pub description: Option<String>,
pub segment_type: SegmentType,
pub rules: Vec<SegmentRule>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UpdateSegment {
pub name: Option<String>,
pub description: Option<Option<String>>,
pub rules: Option<Vec<SegmentRule>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SegmentFilter {
pub segment_type: Option<SegmentType>,
pub name: Option<String>,
pub limit: Option<u32>,
pub offset: Option<u32>,
}
impl Segment {
#[must_use]
pub fn is_dynamic(&self) -> bool {
self.segment_type == SegmentType::Dynamic
}
#[must_use]
pub fn has_rules(&self) -> bool {
!self.rules.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use stateset_primitives::SegmentId;
fn make_segment(segment_type: SegmentType, rules: Vec<SegmentRule>) -> Segment {
Segment {
id: SegmentId::new(),
name: "Test Segment".to_string(),
description: None,
segment_type,
rules,
member_count: 0,
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
fn make_rule(operator: SegmentOperator) -> SegmentRule {
SegmentRule { field: "total_orders".to_string(), operator, value: "5".to_string() }
}
#[test]
fn is_dynamic_returns_true_for_dynamic_segment() {
let segment = make_segment(SegmentType::Dynamic, vec![make_rule(SegmentOperator::Gt)]);
assert!(segment.is_dynamic());
}
#[test]
fn is_dynamic_returns_false_for_static_segment() {
let segment = make_segment(SegmentType::Static, vec![]);
assert!(!segment.is_dynamic());
}
#[test]
fn has_rules_returns_true_when_rules_exist() {
let segment = make_segment(SegmentType::Dynamic, vec![make_rule(SegmentOperator::Eq)]);
assert!(segment.has_rules());
}
#[test]
fn has_rules_returns_false_when_no_rules() {
let segment = make_segment(SegmentType::Static, vec![]);
assert!(!segment.has_rules());
}
#[test]
fn segment_type_display_fromstr_roundtrip() {
for seg_type in [SegmentType::Static, SegmentType::Dynamic] {
let s = seg_type.to_string();
let parsed: SegmentType = s.parse().unwrap();
assert_eq!(parsed, seg_type, "round-trip failed for {s}");
}
}
#[test]
fn segment_operator_key_variants_display_fromstr_roundtrip() {
for op in [
SegmentOperator::Eq,
SegmentOperator::Gt,
SegmentOperator::Contains,
SegmentOperator::Between,
SegmentOperator::Neq,
SegmentOperator::Gte,
SegmentOperator::Lt,
SegmentOperator::Lte,
SegmentOperator::In,
SegmentOperator::StartsWith,
SegmentOperator::EndsWith,
] {
let s = op.to_string();
let parsed: SegmentOperator = s.parse().unwrap();
assert_eq!(parsed, op, "round-trip failed for {s}");
}
}
#[test]
fn segment_type_default_is_static() {
assert_eq!(SegmentType::default(), SegmentType::Static);
}
}