rustpbx 0.3.18

A SIP PBX implementation in Rust
Documentation
use sea_orm::entity::prelude::*;
use sea_orm_migration::prelude::*;
use sea_orm_migration::schema::{
    boolean, integer, json_null, string, string_null, text_null, timestamp, timestamp_null,
};
use sea_orm_migration::sea_query::{ColumnDef, ForeignKeyAction as MigrationForeignKeyAction};
use sea_query::Expr;
use serde::{Deserialize, Serialize};

#[derive(Copy, Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[sea_orm(rs_type = "String", db_type = "Text")]
pub enum RoutingDirection {
    #[sea_orm(string_value = "inbound")]
    Inbound,
    #[sea_orm(string_value = "outbound")]
    Outbound,
}

impl RoutingDirection {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Inbound => "inbound",
            Self::Outbound => "outbound",
        }
    }
}

impl Default for RoutingDirection {
    fn default() -> Self {
        Self::Outbound
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[sea_orm(rs_type = "String", db_type = "Text")]
pub enum RoutingSelectionStrategy {
    #[sea_orm(string_value = "rr")]
    #[serde(alias = "rr", alias = "round_robin", alias = "round-robin")]
    RoundRobin,
    #[sea_orm(string_value = "weight")]
    #[serde(alias = "weight")]
    Weighted,
    #[sea_orm(string_value = "hash")]
    Hash,
}

impl RoutingSelectionStrategy {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::RoundRobin => "rr",
            Self::Weighted => "weight",
            Self::Hash => "hash",
        }
    }
}

impl Default for RoutingSelectionStrategy {
    fn default() -> Self {
        Self::RoundRobin
    }
}

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "rustpbx_routes")]
pub struct Model {
    #[sea_orm(primary_key, auto_increment = true)]
    pub id: i64,
    #[sea_orm(unique)]
    pub name: String,
    pub description: Option<String>,
    pub direction: RoutingDirection,
    pub priority: i32,
    pub is_active: bool,
    pub selection_strategy: RoutingSelectionStrategy,
    pub hash_key: Option<String>,
    pub source_trunk_id: Option<i64>,
    pub default_trunk_id: Option<i64>,
    pub source_pattern: Option<String>,
    pub destination_pattern: Option<String>,
    pub header_filters: Option<Json>,
    pub rewrite_rules: Option<Json>,
    pub target_trunks: Option<Json>,
    pub owner: Option<String>,
    pub notes: Option<Json>,
    pub metadata: Option<Json>,
    pub created_at: DateTimeUtc,
    pub updated_at: DateTimeUtc,
    pub last_deployed_at: Option<DateTimeUtc>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
    #[sea_orm(
        belongs_to = "super::sip_trunk::Entity",
        from = "Column::SourceTrunkId",
        to = "super::sip_trunk::Column::Id",
        on_delete = "SetNull",
        on_update = "Cascade"
    )]
    SourceTrunk,
    #[sea_orm(
        belongs_to = "super::sip_trunk::Entity",
        from = "Column::DefaultTrunkId",
        to = "super::sip_trunk::Column::Id",
        on_delete = "SetNull",
        on_update = "Cascade"
    )]
    DefaultTrunk,
}

impl ActiveModelBehavior for ActiveModel {}

#[cfg(test)]
mod tests {
    use super::RoutingSelectionStrategy;

    #[test]
    fn selection_strategy_accepts_aliases() {
        let rr: RoutingSelectionStrategy = serde_json::from_str("\"rr\"").unwrap();
        assert!(matches!(rr, RoutingSelectionStrategy::RoundRobin));

        let rr_alt: RoutingSelectionStrategy = serde_json::from_str("\"round_robin\"").unwrap();
        assert!(matches!(rr_alt, RoutingSelectionStrategy::RoundRobin));

        let weight: RoutingSelectionStrategy = serde_json::from_str("\"weight\"").unwrap();
        assert!(matches!(weight, RoutingSelectionStrategy::Weighted));
    }

    #[test]
    fn selection_strategy_serializes_with_canonical_names() {
        let serialized = serde_json::to_string(&RoutingSelectionStrategy::RoundRobin).unwrap();
        assert_eq!(serialized, "\"roundrobin\"");

        let serialized_weight = serde_json::to_string(&RoutingSelectionStrategy::Weighted).unwrap();
        assert_eq!(serialized_weight, "\"weighted\"");
    }
}

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .create_table(
                Table::create()
                    .table(Entity)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(Column::Id)
                            .big_integer()
                            .auto_increment()
                            .primary_key(),
                    )
                    .col(string(Column::Name).char_len(160))
                    .col(text_null(Column::Description))
                    .col(
                        string(Column::Direction)
                            .char_len(32)
                            .default(RoutingDirection::default().as_str()),
                    )
                    .col(integer(Column::Priority).not_null().default(100))
                    .col(boolean(Column::IsActive).default(true))
                    .col(
                        string(Column::SelectionStrategy)
                            .char_len(32)
                            .default(RoutingSelectionStrategy::default().as_str()),
                    )
                    .col(string_null(Column::HashKey).char_len(120))
                    .col(ColumnDef::new(Column::SourceTrunkId).big_integer().null())
                    .col(ColumnDef::new(Column::DefaultTrunkId).big_integer().null())
                    .col(string_null(Column::SourcePattern).char_len(160))
                    .col(string_null(Column::DestinationPattern).char_len(160))
                    .col(json_null(Column::HeaderFilters))
                    .col(json_null(Column::RewriteRules))
                    .col(json_null(Column::TargetTrunks))
                    .col(string_null(Column::Owner).char_len(120))
                    .col(json_null(Column::Notes))
                    .col(json_null(Column::Metadata))
                    .col(timestamp(Column::CreatedAt).default(Expr::current_timestamp()))
                    .col(timestamp(Column::UpdatedAt).default(Expr::current_timestamp()))
                    .col(timestamp_null(Column::LastDeployedAt))
                    .foreign_key(
                        ForeignKey::create()
                            .name("fk_routes_source_trunk")
                            .from(Entity, Column::SourceTrunkId)
                            .to(super::sip_trunk::Entity, super::sip_trunk::Column::Id)
                            .on_delete(MigrationForeignKeyAction::SetNull)
                            .on_update(MigrationForeignKeyAction::Cascade),
                    )
                    .foreign_key(
                        ForeignKey::create()
                            .name("fk_routes_default_trunk")
                            .from(Entity, Column::DefaultTrunkId)
                            .to(super::sip_trunk::Entity, super::sip_trunk::Column::Id)
                            .on_delete(MigrationForeignKeyAction::SetNull)
                            .on_update(MigrationForeignKeyAction::Cascade),
                    )
                    .to_owned(),
            )
            .await?;

        manager
            .create_index(
                Index::create()
                    .name("idx_rustpbx_routes_name")
                    .table(Entity)
                    .col(Column::Name)
                    .unique()
                    .to_owned(),
            )
            .await?;

        manager
            .create_index(
                Index::create()
                    .name("idx_rustpbx_routes_direction")
                    .table(Entity)
                    .col(Column::Direction)
                    .col(Column::IsActive)
                    .to_owned(),
            )
            .await?;

        manager
            .create_index(
                Index::create()
                    .name("idx_rustpbx_routes_priority")
                    .table(Entity)
                    .col(Column::Priority)
                    .to_owned(),
            )
            .await
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .drop_table(Table::drop().table(Entity).to_owned())
            .await
    }
}