From 14acc7e2eb98ff10f5bbac0844fce1c8574ff158 Mon Sep 17 00:00:00 2001 From: angrynode Date: Sat, 15 Nov 2025 11:35:56 +0100 Subject: [PATCH 01/10] feat: Allow usage of MagnetLink in sea_orm::Model --- Cargo.toml | 10 ++++++++ src/magnet.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/sea_orm.rs | 19 ++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 tests/sea_orm.rs diff --git a/Cargo.toml b/Cargo.toml index 19b7143..4a163d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ sha256 = "1.5" rustc-hex = "2.1" serde = { version = "1", features = [ "derive" ] } fluent-uri = { version = "0.4", features = [ "serde" ] } +# For Sea-ORM integration +sea-orm = { version = "2.0.0-rc.19", optional = true } [dev-dependencies] serde_json = "1" @@ -33,6 +35,8 @@ serde_json = "1" [features] magnet_force_name = [] unknown_tracker_scheme = [] +sea_orm = [ "dep:sea-orm" ] +test_sea_orm = [ "dep:sea-orm", "sea-orm/sqlx-sqlite" ] [[test]] name = "magnet_force_name" @@ -45,3 +49,9 @@ name = "unknown_tracker_scheme" path = "tests/unknown_tracker_scheme.rs" required-features = [ "unknown_tracker_scheme" ] test = true + +[[test]] +name = "sea_orm" +path = "tests/sea_orm.rs" +required-features = [ "sea_orm" ] +test = true \ No newline at end of file diff --git a/src/magnet.rs b/src/magnet.rs index cd2c8b7..2251624 100644 --- a/src/magnet.rs +++ b/src/magnet.rs @@ -322,6 +322,70 @@ impl std::fmt::Display for MagnetLink { } } +impl PartialEq for MagnetLink { + fn eq(&self, other: &Self) -> bool { + self.query == other.query + } +} + +#[cfg(feature = "sea_orm")] +impl From for sea_orm::sea_query::Value { + fn from(m: MagnetLink) -> Self { + Self::String(Some(m.to_string())) + } +} + +#[cfg(feature = "sea_orm")] +impl sea_orm::TryGetable for MagnetLink { + fn try_get_by( + res: &sea_orm::QueryResult, + index: I, + ) -> Result { + let val: String = res.try_get_by(index)?; + MagnetLink::new(&val).map_err(|e| { + sea_orm::error::TryGetError::DbErr(sea_orm::DbErr::TryIntoErr { + from: "String", + into: "MagnetLink", + source: std::sync::Arc::new(e), + }) + }) + } +} + +#[cfg(feature = "sea_orm")] +impl sea_orm::sea_query::ValueType for MagnetLink { + fn try_from(v: sea_orm::Value) -> Result { + match v { + // TODO: What to do with None String? + // This should probably work with Option but not with MagnetLink + // but i have no idea how sea orm works... + sea_orm::Value::String(Some(s)) => { + MagnetLink::new(&s).map_err(|_e| sea_orm::sea_query::ValueTypeErr) + } + _ => Err(sea_orm::sea_query::ValueTypeErr), + } + } + + fn type_name() -> String { + "MagnetLink".to_string() + } + + fn array_type() -> sea_orm::sea_query::ArrayType { + sea_orm::sea_query::ArrayType::String + } + + fn column_type() -> sea_orm::sea_query::ColumnType { + sea_orm::sea_query::ColumnType::String(sea_orm::sea_query::table::StringLen::None) + } +} + +#[cfg(feature = "sea_orm")] +impl sea_orm::sea_query::Nullable for MagnetLink { + fn null() -> sea_orm::sea_query::Value { + sea_orm::sea_query::Value::String(None) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/sea_orm.rs b/tests/sea_orm.rs new file mode 100644 index 0000000..661885a --- /dev/null +++ b/tests/sea_orm.rs @@ -0,0 +1,19 @@ +use hightorrent::MagnetLink; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "magnet")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub torrent_id: String, + pub magnet: MagnetLink, + pub name: String, + pub resolved: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +#[async_trait::async_trait] +impl ActiveModelBehavior for ActiveModel {} From a8930fbc99f5cb6a1d05fd7898a3a5ef0c08b3d5 Mon Sep 17 00:00:00 2001 From: angrynode Date: Sat, 15 Nov 2025 15:50:35 +0100 Subject: [PATCH 02/10] feat: Support TorrentID in sea_orm::Model --- src/id.rs | 58 ++++++++++++++++++++++++++++++++++++ tests/sea_orm.rs | 76 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 120 insertions(+), 14 deletions(-) diff --git a/src/id.rs b/src/id.rs index 8953e34..8d7dcde 100644 --- a/src/id.rs +++ b/src/id.rs @@ -60,3 +60,61 @@ impl FromStr for TorrentID { Ok(Self::from_infohash(&hash)) } } + +#[cfg(feature = "sea_orm")] +impl From for sea_orm::sea_query::Value { + fn from(id: TorrentID) -> Self { + Self::String(Some(id.to_string())) + } +} + +#[cfg(feature = "sea_orm")] +impl sea_orm::TryGetable for TorrentID { + fn try_get_by( + res: &sea_orm::QueryResult, + index: I, + ) -> Result { + let val: String = res.try_get_by(index)?; + TorrentID::new(&val).map_err(|e| { + sea_orm::error::TryGetError::DbErr(sea_orm::DbErr::TryIntoErr { + from: "String", + into: "TorrentID", + source: std::sync::Arc::new(e), + }) + }) + } +} + +#[cfg(feature = "sea_orm")] +impl sea_orm::sea_query::ValueType for TorrentID { + fn try_from(v: sea_orm::Value) -> Result { + match v { + // TODO: What to do with None String? + // This should probably work with Option but not with MagnetLink + // but i have no idea how sea orm works... + sea_orm::Value::String(Some(s)) => { + TorrentID::new(&s).map_err(|_e| sea_orm::sea_query::ValueTypeErr) + } + _ => Err(sea_orm::sea_query::ValueTypeErr), + } + } + + fn type_name() -> String { + "TorrentID".to_string() + } + + fn array_type() -> sea_orm::sea_query::ArrayType { + sea_orm::sea_query::ArrayType::String + } + + fn column_type() -> sea_orm::sea_query::ColumnType { + sea_orm::sea_query::ColumnType::String(sea_orm::sea_query::table::StringLen::N(40)) + } +} + +#[cfg(feature = "sea_orm")] +impl sea_orm::sea_query::Nullable for TorrentID { + fn null() -> sea_orm::sea_query::Value { + sea_orm::sea_query::Value::String(None) + } +} diff --git a/tests/sea_orm.rs b/tests/sea_orm.rs index 661885a..627678e 100644 --- a/tests/sea_orm.rs +++ b/tests/sea_orm.rs @@ -1,19 +1,67 @@ -use hightorrent::MagnetLink; +use hightorrent::{MagnetLink, TorrentID}; use sea_orm::entity::prelude::*; +use sea_orm::*; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] -#[sea_orm(table_name = "magnet")] -pub struct Model { - #[sea_orm(primary_key)] - pub id: i32, - pub torrent_id: String, - pub magnet: MagnetLink, - pub name: String, - pub resolved: bool, +mod id { + use super::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "magnet")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub torrent_id: TorrentID, + pub magnet: String, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + #[async_trait::async_trait] + impl ActiveModelBehavior for ActiveModel {} +} + +mod magnet { + use super::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "magnet")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub torrent_id: String, + pub magnet: MagnetLink, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + #[async_trait::async_trait] + impl ActiveModelBehavior for ActiveModel {} +} + +#[test] +fn test_magnet_active_model() { + let magnet = + MagnetLink::new(&std::fs::read_to_string("tests/bittorrent-v2-test.magnet").unwrap()) + .unwrap(); + + let _model = magnet::ActiveModel { + torrent_id: Set(magnet.id().to_string()), + magnet: Set(magnet.clone()), + ..Default::default() + }; } -#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +#[test] +fn test_torrentid_active_model() { + let magnet = + MagnetLink::new(&std::fs::read_to_string("tests/bittorrent-v2-test.magnet").unwrap()) + .unwrap(); -#[async_trait::async_trait] -impl ActiveModelBehavior for ActiveModel {} + let _model = id::ActiveModel { + torrent_id: Set(magnet.id()), + magnet: Set(magnet.to_string()), + ..Default::default() + }; +} From b3fc473cd2c1cce2139d9f317cadc35e43b20df4 Mon Sep 17 00:00:00 2001 From: angrynode Date: Sun, 16 Nov 2025 13:26:55 +0100 Subject: [PATCH 03/10] tests: Add real-world SQLite test for sea_orm integration --- Cargo.toml | 7 +- src/id.rs | 59 +------ src/magnet.rs | 62 +------- tests/sea_orm.rs | 400 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 414 insertions(+), 114 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4a163d0..6106f65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,9 @@ serde = { version = "1", features = [ "derive" ] } fluent-uri = { version = "0.4", features = [ "serde" ] } # For Sea-ORM integration sea-orm = { version = "2.0.0-rc.19", optional = true } +sea-orm-migration = { version = "2.0.0-rc.19", optional = true } +async-tempfile = { version = "0.7", optional = true } +tokio = { version = "1", optional = true } [dev-dependencies] serde_json = "1" @@ -36,7 +39,7 @@ serde_json = "1" magnet_force_name = [] unknown_tracker_scheme = [] sea_orm = [ "dep:sea-orm" ] -test_sea_orm = [ "dep:sea-orm", "sea-orm/sqlx-sqlite" ] +test_sea_orm = [ "dep:sea-orm", "sea-orm/sqlx-sqlite", "dep:tokio", "tokio/macros", "tokio/rt", "dep:async-tempfile", "dep:sea-orm-migration", "sea-orm-migration/runtime-tokio" ] [[test]] name = "magnet_force_name" @@ -53,5 +56,5 @@ test = true [[test]] name = "sea_orm" path = "tests/sea_orm.rs" -required-features = [ "sea_orm" ] +required-features = [ "sea_orm", "test_sea_orm" ] test = true \ No newline at end of file diff --git a/src/id.rs b/src/id.rs index 8d7dcde..5c2dda1 100644 --- a/src/id.rs +++ b/src/id.rs @@ -17,6 +17,7 @@ use crate::{InfoHash, InfoHashError}; /// [`TorrentID::from_infohash`](crate::id::TorrentID::from_infohash) and /// [`InfoHash::id`](crate::hash::InfoHash::id) methods. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "sea_orm", derive(sea_orm::DeriveValueType))] pub struct TorrentID(String); impl TorrentID { @@ -60,61 +61,3 @@ impl FromStr for TorrentID { Ok(Self::from_infohash(&hash)) } } - -#[cfg(feature = "sea_orm")] -impl From for sea_orm::sea_query::Value { - fn from(id: TorrentID) -> Self { - Self::String(Some(id.to_string())) - } -} - -#[cfg(feature = "sea_orm")] -impl sea_orm::TryGetable for TorrentID { - fn try_get_by( - res: &sea_orm::QueryResult, - index: I, - ) -> Result { - let val: String = res.try_get_by(index)?; - TorrentID::new(&val).map_err(|e| { - sea_orm::error::TryGetError::DbErr(sea_orm::DbErr::TryIntoErr { - from: "String", - into: "TorrentID", - source: std::sync::Arc::new(e), - }) - }) - } -} - -#[cfg(feature = "sea_orm")] -impl sea_orm::sea_query::ValueType for TorrentID { - fn try_from(v: sea_orm::Value) -> Result { - match v { - // TODO: What to do with None String? - // This should probably work with Option but not with MagnetLink - // but i have no idea how sea orm works... - sea_orm::Value::String(Some(s)) => { - TorrentID::new(&s).map_err(|_e| sea_orm::sea_query::ValueTypeErr) - } - _ => Err(sea_orm::sea_query::ValueTypeErr), - } - } - - fn type_name() -> String { - "TorrentID".to_string() - } - - fn array_type() -> sea_orm::sea_query::ArrayType { - sea_orm::sea_query::ArrayType::String - } - - fn column_type() -> sea_orm::sea_query::ColumnType { - sea_orm::sea_query::ColumnType::String(sea_orm::sea_query::table::StringLen::N(40)) - } -} - -#[cfg(feature = "sea_orm")] -impl sea_orm::sea_query::Nullable for TorrentID { - fn null() -> sea_orm::sea_query::Value { - sea_orm::sea_query::Value::String(None) - } -} diff --git a/src/magnet.rs b/src/magnet.rs index 2251624..9b8d896 100644 --- a/src/magnet.rs +++ b/src/magnet.rs @@ -3,6 +3,8 @@ use fluent_uri::{ParseError as UriParseError, Uri}; use crate::{InfoHash, InfoHashError, TorrentID, Tracker, TrackerError}; +use std::str::FromStr; + /// Error occurred during parsing a [`MagnetLink`](crate::magnet::MagnetLink). #[derive(Clone, Debug, PartialEq)] pub enum MagnetLinkError { @@ -119,6 +121,8 @@ impl std::error::Error for MagnetLinkError { /// More information is specified in [BEP-0009](https://bittorrent.org/beps/bep_0009.html), and /// even more appears in the wild, as explained [on Wikipedia](https://en.wikipedia.org/wiki/Magnet_URI_scheme). #[derive(Clone, Debug)] +#[cfg_attr(feature = "sea_orm", derive(sea_orm::DeriveValueType))] +#[cfg_attr(feature = "sea_orm", sea_orm(value_type = "String"))] pub struct MagnetLink { /// Only mandatory field for magnet link parsing, unless the /// `magnet_force_name` crate feature is enabled. @@ -328,61 +332,11 @@ impl PartialEq for MagnetLink { } } -#[cfg(feature = "sea_orm")] -impl From for sea_orm::sea_query::Value { - fn from(m: MagnetLink) -> Self { - Self::String(Some(m.to_string())) - } -} - -#[cfg(feature = "sea_orm")] -impl sea_orm::TryGetable for MagnetLink { - fn try_get_by( - res: &sea_orm::QueryResult, - index: I, - ) -> Result { - let val: String = res.try_get_by(index)?; - MagnetLink::new(&val).map_err(|e| { - sea_orm::error::TryGetError::DbErr(sea_orm::DbErr::TryIntoErr { - from: "String", - into: "MagnetLink", - source: std::sync::Arc::new(e), - }) - }) - } -} - -#[cfg(feature = "sea_orm")] -impl sea_orm::sea_query::ValueType for MagnetLink { - fn try_from(v: sea_orm::Value) -> Result { - match v { - // TODO: What to do with None String? - // This should probably work with Option but not with MagnetLink - // but i have no idea how sea orm works... - sea_orm::Value::String(Some(s)) => { - MagnetLink::new(&s).map_err(|_e| sea_orm::sea_query::ValueTypeErr) - } - _ => Err(sea_orm::sea_query::ValueTypeErr), - } - } - - fn type_name() -> String { - "MagnetLink".to_string() - } - - fn array_type() -> sea_orm::sea_query::ArrayType { - sea_orm::sea_query::ArrayType::String - } - - fn column_type() -> sea_orm::sea_query::ColumnType { - sea_orm::sea_query::ColumnType::String(sea_orm::sea_query::table::StringLen::None) - } -} +impl FromStr for MagnetLink { + type Err = MagnetLinkError; -#[cfg(feature = "sea_orm")] -impl sea_orm::sea_query::Nullable for MagnetLink { - fn null() -> sea_orm::sea_query::Value { - sea_orm::sea_query::Value::String(None) + fn from_str(s: &str) -> Result { + Self::new(s) } } diff --git a/tests/sea_orm.rs b/tests/sea_orm.rs index 627678e..e679c95 100644 --- a/tests/sea_orm.rs +++ b/tests/sea_orm.rs @@ -40,6 +40,218 @@ mod magnet { impl ActiveModelBehavior for ActiveModel {} } +pub mod mixed_magnet { + use super::*; + use sea_orm_migration::prelude::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "mixed_magnet")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(unique)] + pub torrent_id: TorrentID, + #[sea_orm(unique)] + pub magnet: MagnetLink, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + #[async_trait::async_trait] + impl ActiveModelBehavior for ActiveModel {} + + pub mod migration { + pub mod m20251115_01_mixed_magnet { + use sea_orm_migration::{prelude::*, schema::*}; + + #[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(MixedMagnet::Table) + .if_not_exists() + .col(pk_auto(MixedMagnet::Id)) + .col(string(MixedMagnet::TorrentID).unique_key()) + .col(string(MixedMagnet::Magnet).unique_key()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(MixedMagnet::Table).to_owned()) + .await + } + } + + #[derive(DeriveIden)] + enum MixedMagnet { + Table, + Id, + TorrentID, + Magnet, + } + } + } + + pub struct Migrator; + + #[async_trait::async_trait] + impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(migration::m20251115_01_mixed_magnet::Migration)] + } + } +} + +pub mod mixed_torrent { + use super::*; + use sea_orm_migration::prelude::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "mixed_torrent")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(unique)] + pub torrent_id: TorrentID, + #[sea_orm(unique)] + pub torrent_file: TorrentFile, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + #[async_trait::async_trait] + impl ActiveModelBehavior for ActiveModel {} + + pub mod migration { + pub mod m20251115_01_mixed_torrent { + use sea_orm_migration::{prelude::*, schema::*}; + + #[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(MixedTorrent::Table) + .if_not_exists() + .col(pk_auto(MixedTorrent::Id)) + .col(string(MixedTorrent::TorrentID).unique_key()) + .col(var_binary(MixedTorrent::TorrentFile, 0).unique_key()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(MixedTorrent::Table).to_owned()) + .await + } + } + + #[derive(DeriveIden)] + enum MixedTorrent { + Table, + Id, + TorrentID, + TorrentFile, + } + } + } + + pub struct Migrator; + + #[async_trait::async_trait] + impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(migration::m20251115_01_mixed_torrent::Migration)] + } + } +} + +pub mod optional_mixed_magnet { + use super::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "optional_mixed_magnet")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub torrent_id: Option, + pub magnet: Option, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + #[async_trait::async_trait] + impl ActiveModelBehavior for ActiveModel {} + + pub mod migration { + use sea_orm_migration::prelude::*; + + pub mod m20251118_01_optional_mixed_magnet { + use sea_orm_migration::{prelude::*, schema::*}; + + #[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(OptionalMixedMagnet::Table) + .if_not_exists() + .col(pk_auto(OptionalMixedMagnet::Id)) + .col(string_null(OptionalMixedMagnet::TorrentID)) + .col(string_null(OptionalMixedMagnet::Magnet)) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(OptionalMixedMagnet::Table).to_owned()) + .await + } + } + + #[derive(DeriveIden)] + enum OptionalMixedMagnet { + Table, + Id, + TorrentID, + Magnet, + } + } + + pub struct Migrator; + + #[async_trait::async_trait] + impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20251118_01_optional_mixed_magnet::Migration)] + } + } + } +} + #[test] fn test_magnet_active_model() { let magnet = @@ -65,3 +277,191 @@ fn test_torrentid_active_model() { ..Default::default() }; } + +#[tokio::test] +async fn test_magnet_real_db() { + use sea_orm_migration::*; + + let tmpdir = async_tempfile::TempDir::new().await.unwrap(); + let sqlite = tmpdir.join("mixed_magnet.sqlite"); + let sqlite_str = sqlite.to_str().unwrap(); + + let db = sea_orm::Database::connect(&format!("sqlite://{}?mode=rwc", sqlite_str)) + .await + .unwrap(); + mixed_magnet::Migrator::up(&db, None).await.unwrap(); + + let magnet = + MagnetLink::new(&std::fs::read_to_string("tests/bittorrent-v2-test.magnet").unwrap()) + .unwrap(); + + let model = mixed_magnet::ActiveModel { + torrent_id: Set(magnet.id()), + magnet: Set(magnet.clone()), + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let magnet2 = MagnetLink::new( + &std::fs::read_to_string("tests/bittorrent-v2-hybrid-test.magnet").unwrap(), + ) + .unwrap(); + + let model2 = mixed_magnet::ActiveModel { + torrent_id: Set(magnet2.id()), + magnet: Set(magnet2.clone()), + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let nonactive_model = model.try_into_model().unwrap(); + let saved_model_by_id = mixed_magnet::Entity::find_by_id(nonactive_model.id) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id, nonactive_model); + assert_eq!(saved_model_by_id.magnet, magnet); + assert_eq!(nonactive_model.magnet, magnet); + + let nonactive_model2 = model2.try_into_model().unwrap(); + let saved_model_by_id2 = mixed_magnet::Entity::find_by_id(nonactive_model2.id) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id2, nonactive_model2); + assert_eq!(saved_model_by_id2.magnet, magnet2); + assert_eq!(nonactive_model2.magnet, magnet2); + + // Try query by TorrentID + let saved_model_by_torrentid = mixed_magnet::Entity::find() + .filter(mixed_magnet::Column::TorrentId.eq(magnet.id())) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id, nonactive_model); + assert_eq!(saved_model_by_torrentid.magnet, magnet); + + // Try query by MagnetLink + let saved_model_by_magnet = mixed_magnet::Entity::find() + .filter(mixed_magnet::Column::Magnet.eq(magnet.clone())) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id, nonactive_model); + assert_eq!(saved_model_by_magnet.magnet, magnet); + + // Try listing torrents + let mut found_one = false; + let mut found_two = false; + let list = mixed_magnet::Entity::find().all(&db).await.unwrap(); + println!("{:?}", list); + for entry in list { + if entry.magnet == magnet && entry.torrent_id == magnet.id() { + found_one = true; + continue; + } + + if entry.magnet == magnet2 && entry.torrent_id == magnet2.id() { + found_two = true; + } + } + + assert!(found_one); + assert!(found_two); +} + +#[tokio::test] +async fn test_torrent_real_optional_none() { + use sea_orm_migration::*; + + let tmpdir = async_tempfile::TempDir::new().await.unwrap(); + let sqlite = tmpdir.join("optional_mixed_none.sqlite"); + let sqlite_str = sqlite.to_str().unwrap(); + + let db = sea_orm::Database::connect(&format!("sqlite://{}?mode=rwc", sqlite_str)) + .await + .unwrap(); + optional_mixed_magnet::migration::Migrator::up(&db, None) + .await + .unwrap(); + + // Try with None + let model = optional_mixed_magnet::ActiveModel { + torrent_id: Set(None), + magnet: Set(None), + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let list = optional_mixed_magnet::Entity::find() + .all(&db) + .await + .unwrap(); + for entry in list { + println!("- {:?}", entry); + } + + let nonactive_model = model.try_into_model().unwrap(); + let saved_model = optional_mixed_magnet::Entity::find() + .filter(optional_mixed_magnet::Column::Magnet.is_null()) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model.magnet.as_ref(), None); + assert_eq!(nonactive_model.magnet.as_ref(), None); +} + +#[tokio::test] +async fn test_torrent_real_optional_notset() { + use sea_orm_migration::*; + + let tmpdir = async_tempfile::TempDir::new().await.unwrap(); + let sqlite = tmpdir.join("optional_mixed_magnet_none.sqlite"); + let sqlite_str = sqlite.to_str().unwrap(); + + let db = sea_orm::Database::connect(&format!("sqlite://{}?mode=rwc", sqlite_str)) + .await + .unwrap(); + optional_mixed_magnet::migration::Migrator::up(&db, None) + .await + .unwrap(); + + // Try with None + let model = optional_mixed_magnet::ActiveModel { + torrent_id: NotSet, + magnet: NotSet, + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let list = optional_mixed_magnet::Entity::find() + .all(&db) + .await + .unwrap(); + for entry in list { + println!("- {:?}", entry); + } + + let nonactive_model = model.try_into_model().unwrap(); + let saved_model = optional_mixed_magnet::Entity::find() + .filter(optional_mixed_magnet::Column::Magnet.is_null()) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model.magnet.as_ref(), None); + assert_eq!(nonactive_model.magnet.as_ref(), None); +} From b39c0abe2f859a2393920ecf74c086ba341fa8cc Mon Sep 17 00:00:00 2001 From: angrynode Date: Thu, 27 Nov 2025 17:14:48 +0100 Subject: [PATCH 04/10] feat: More PartialEq types --- CHANGELOG.md | 4 ++++ src/torrent_file.rs | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a94eb12..83e97fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## UNRELEASED (YYYY-MM-DD) +### Added + +- `TorrentFile`, `DecodedTorrent` and `DecodedInfo` implement `PartialEq` + ## Version 0.4.0 (2025-11-10) This release is focused on stricter parsing of torrents and magnets, and diff --git a/src/torrent_file.rs b/src/torrent_file.rs index aca58a8..a0ffa95 100644 --- a/src/torrent_file.rs +++ b/src/torrent_file.rs @@ -85,7 +85,7 @@ impl std::error::Error for TorrentFileError { /// ```ignore /// std::fs::write("export.torrent", &torrent.to_vec()).unwrap(); /// ``` -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct TorrentFile { pub hash: InfoHash, pub name: String, @@ -98,7 +98,7 @@ pub struct TorrentFile { /// In its present form, DecodedTorrent mostly cares about the info dict, but preserves other fields /// as [`BencodeValue`](bt_bencode::BencodeValue) in an `extra` mapping so you can implement /// your own extra parsing. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct DecodedTorrent { /// Main tracker #[serde(skip_serializing_if = "Option::is_none")] @@ -219,7 +219,7 @@ impl UnsafeV1FileContent { /// mapping so you can implement your own extra parsing. // bt_bencode does not support serializing None options and empty HashMaps, so we skip // serialization in those cases. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct DecodedInfo { #[serde(rename = "meta version")] #[serde(skip_serializing_if = "Option::is_none")] From 20290bbcfa215be62d9b7e47c7ec893b4df51e5c Mon Sep 17 00:00:00 2001 From: angrynode Date: Thu, 27 Nov 2025 17:23:42 +0100 Subject: [PATCH 05/10] feat: sea_orm support for TorrentFile --- CHANGELOG.md | 2 + src/torrent_file.rs | 57 +++++++++++++++++++++++++ tests/sea_orm.rs | 101 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 159 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83e97fd..516b2ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- the `sea_orm` crate feature allows to use `MagnetLink`, `TorrentFile` and `TorrentID` + in sea_orm models - `TorrentFile`, `DecodedTorrent` and `DecodedInfo` implement `PartialEq` ## Version 0.4.0 (2025-11-10) diff --git a/src/torrent_file.rs b/src/torrent_file.rs index a0ffa95..974c770 100644 --- a/src/torrent_file.rs +++ b/src/torrent_file.rs @@ -1,5 +1,7 @@ use bt_bencode::Value as BencodeValue; use rustc_hex::ToHex; +#[cfg(feature = "sea_orm")] +use sea_orm::prelude::*; use serde::{Deserialize, Serialize}; use sha1::{Digest, Sha1}; @@ -330,6 +332,61 @@ impl TorrentFile { } } +#[cfg(feature = "sea_orm")] +impl From for sea_orm::sea_query::Value { + fn from(t: TorrentFile) -> Self { + Value::Bytes(Some(t.to_vec())) + } +} + +#[cfg(feature = "sea_orm")] +impl sea_orm::TryGetable for TorrentFile { + fn try_get_by( + res: &sea_orm::QueryResult, + index: I, + ) -> Result { + let val: Vec = res.try_get_by(index)?; + TorrentFile::from_slice(&val).map_err(|e| { + sea_orm::error::TryGetError::DbErr(sea_orm::DbErr::TryIntoErr { + from: "Bytes", + into: "TorrentFile", + source: std::sync::Arc::new(e), + }) + }) + } +} + +#[cfg(feature = "sea_orm")] +impl sea_orm::sea_query::ValueType for TorrentFile { + fn try_from(v: sea_orm::Value) -> Result { + match v { + sea_orm::Value::Bytes(Some(s)) => { + TorrentFile::from_slice(&s).map_err(|_e| sea_orm::sea_query::ValueTypeErr) + } + _ => Err(sea_orm::sea_query::ValueTypeErr), + } + } + + fn type_name() -> String { + "TorrentFile".to_string() + } + + fn array_type() -> sea_orm::sea_query::ArrayType { + sea_orm::sea_query::ArrayType::Bytes + } + + fn column_type() -> sea_orm::sea_query::ColumnType { + sea_orm::sea_query::ColumnType::VarBinary(StringLen::None) + } +} + +#[cfg(feature = "sea_orm")] +impl sea_orm::sea_query::Nullable for TorrentFile { + fn null() -> sea_orm::sea_query::Value { + sea_orm::sea_query::Value::Bytes(None) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/sea_orm.rs b/tests/sea_orm.rs index e679c95..bbb4b6f 100644 --- a/tests/sea_orm.rs +++ b/tests/sea_orm.rs @@ -1,4 +1,4 @@ -use hightorrent::{MagnetLink, TorrentID}; +use hightorrent::{MagnetLink, TorrentFile, TorrentID}; use sea_orm::entity::prelude::*; use sea_orm::*; @@ -465,3 +465,102 @@ async fn test_torrent_real_optional_notset() { assert_eq!(saved_model.magnet.as_ref(), None); assert_eq!(nonactive_model.magnet.as_ref(), None); } + +#[tokio::test] +async fn test_torrent_real_db() { + use sea_orm_migration::*; + + let tmpdir = async_tempfile::TempDir::new().await.unwrap(); + let sqlite = tmpdir.join("mixed.sqlite"); + let sqlite_str = sqlite.to_str().unwrap(); + + let db = sea_orm::Database::connect(&format!("sqlite://{}?mode=rwc", sqlite_str)) + .await + .unwrap(); + mixed_torrent::Migrator::up(&db, None).await.unwrap(); + + let torrent = + TorrentFile::from_slice(&std::fs::read("tests/bittorrent-v2-test.torrent").unwrap()) + .unwrap(); + + let model = mixed_torrent::ActiveModel { + torrent_id: Set(torrent.id()), + torrent_file: Set(torrent.clone()), + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let torrent2 = + TorrentFile::from_slice(&std::fs::read("tests/bittorrent-v2-hybrid-test.torrent").unwrap()) + .unwrap(); + + let model2 = mixed_torrent::ActiveModel { + torrent_id: Set(torrent2.id()), + torrent_file: Set(torrent2.clone()), + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let nonactive_model = model.try_into_model().unwrap(); + let saved_model_by_id = mixed_torrent::Entity::find_by_id(nonactive_model.id) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id, nonactive_model); + assert_eq!(saved_model_by_id.torrent_file, torrent); + assert_eq!(nonactive_model.torrent_file, torrent); + + let nonactive_model2 = model2.try_into_model().unwrap(); + let saved_model_by_id2 = mixed_torrent::Entity::find_by_id(nonactive_model2.id) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id2, nonactive_model2); + assert_eq!(saved_model_by_id2.torrent_file, torrent2); + assert_eq!(nonactive_model2.torrent_file, torrent2); + + // Try query by TorrentID + let saved_model_by_torrentid = mixed_torrent::Entity::find() + .filter(mixed_torrent::Column::TorrentId.eq(torrent.id())) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id, nonactive_model); + assert_eq!(saved_model_by_torrentid.torrent_file, torrent); + + // Try query by TorrentFile + let saved_model_by_torrent = mixed_torrent::Entity::find() + .filter(mixed_torrent::Column::TorrentFile.eq(torrent.clone())) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model_by_id, nonactive_model); + assert_eq!(saved_model_by_torrent.torrent_file, torrent); + + // Try listing torrents + let mut found_one = false; + let mut found_two = false; + let list = mixed_torrent::Entity::find().all(&db).await.unwrap(); + println!("{:?}", list); + for entry in list { + if entry.torrent_file == torrent && entry.torrent_id == torrent.id() { + found_one = true; + continue; + } + + if entry.torrent_file == torrent2 && entry.torrent_id == torrent2.id() { + found_two = true; + } + } + + assert!(found_one); + assert!(found_two); +} From c912d1b991c8219d76fc4e5d0e2c73693d62178c Mon Sep 17 00:00:00 2001 From: angrynode Date: Wed, 3 Dec 2025 11:38:12 +0100 Subject: [PATCH 06/10] deps: Update to sea-orm-migration RC20 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6106f65..cdb0a97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ serde = { version = "1", features = [ "derive" ] } fluent-uri = { version = "0.4", features = [ "serde" ] } # For Sea-ORM integration sea-orm = { version = "2.0.0-rc.19", optional = true } -sea-orm-migration = { version = "2.0.0-rc.19", optional = true } +sea-orm-migration = { version = "2.0.0-rc.20", optional = true } async-tempfile = { version = "0.7", optional = true } tokio = { version = "1", optional = true } From 900bb1c2d356dea46196a472df4c5fcbc29de75d Mon Sep 17 00:00:00 2001 From: angrynode Date: Wed, 3 Dec 2025 11:41:23 +0100 Subject: [PATCH 07/10] minor: cargo clippy --- src/torrent_file.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/torrent_file.rs b/src/torrent_file.rs index 974c770..98afac6 100644 --- a/src/torrent_file.rs +++ b/src/torrent_file.rs @@ -125,20 +125,10 @@ pub struct DecodedTorrent { impl DecodedTorrent { pub fn files(&self) -> Result, TorrentFileError> { - if self.info.files.is_none() { - if self.info.file_tree.is_none() { - // V1 torrent with single file - Ok(vec![TorrentContent { - path: PathBuf::from(&self.info.name), - size: self.info.length.unwrap(), - }]) - } else { - todo!("v2 torrent files"); - } - } else { + if let Some(info_files) = &self.info.files { // V1 torrent with multiple files let mut files: Vec = vec![]; - for file in self.info.files.as_ref().unwrap() { + for file in info_files { // TODO: error let f: UnsafeV1FileContent = bt_bencode::from_value(file.clone()).unwrap(); if let Some(parsed_file) = f.to_torrent_content()? { @@ -148,8 +138,18 @@ impl DecodedTorrent { // Sort files by alphabetical order files.sort(); - Ok(files) + return Ok(files); + } + + if let Some(_info_file_tree) = &self.info.file_tree { + todo!("v2 torrent files"); } + + // V1 torrent with single file + Ok(vec![TorrentContent { + path: PathBuf::from(&self.info.name), + size: self.info.length.unwrap(), + }]) } } From 5a00244ce72aa546be7de6aca0ef5a0c880f686f Mon Sep 17 00:00:00 2001 From: angrynode Date: Wed, 3 Dec 2025 11:44:09 +0100 Subject: [PATCH 08/10] docs: Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 516b2ae..ffd93eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - the `sea_orm` crate feature allows to use `MagnetLink`, `TorrentFile` and `TorrentID` in sea_orm models - `TorrentFile`, `DecodedTorrent` and `DecodedInfo` implement `PartialEq` +- `MagnetFile` implements `FromStr` ## Version 0.4.0 (2025-11-10) From b13e16aaffe1e95d57a237191e9fa7f5707a4f70 Mon Sep 17 00:00:00 2001 From: angrynode Date: Wed, 3 Dec 2025 11:55:45 +0100 Subject: [PATCH 09/10] feat: MagnetLink supports serde de/serialization --- CHANGELOG.md | 3 ++- src/magnet.rs | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffd93eb..bb36a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - the `sea_orm` crate feature allows to use `MagnetLink`, `TorrentFile` and `TorrentID` in sea_orm models - `TorrentFile`, `DecodedTorrent` and `DecodedInfo` implement `PartialEq` -- `MagnetFile` implements `FromStr` +- `MagnetFile` implements `FromStr`, `TryFrom`, and `Into` +- `MagnetFile` supports serde de/serialization ## Version 0.4.0 (2025-11-10) diff --git a/src/magnet.rs b/src/magnet.rs index 9b8d896..016ece6 100644 --- a/src/magnet.rs +++ b/src/magnet.rs @@ -120,9 +120,11 @@ impl std::error::Error for MagnetLinkError { /// /// More information is specified in [BEP-0009](https://bittorrent.org/beps/bep_0009.html), and /// even more appears in the wild, as explained [on Wikipedia](https://en.wikipedia.org/wiki/Magnet_URI_scheme). -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(feature = "sea_orm", derive(sea_orm::DeriveValueType))] #[cfg_attr(feature = "sea_orm", sea_orm(value_type = "String"))] +#[serde(try_from = "String")] +#[serde(into = "String")] pub struct MagnetLink { /// Only mandatory field for magnet link parsing, unless the /// `magnet_force_name` crate feature is enabled. @@ -340,6 +342,20 @@ impl FromStr for MagnetLink { } } +impl TryFrom for MagnetLink { + type Error = MagnetLinkError; + + fn try_from(s: String) -> Result { + Self::new(&s) + } +} + +impl From for String { + fn from(m: MagnetLink) -> Self { + m.to_string() + } +} + #[cfg(test)] mod tests { use super::*; @@ -579,4 +595,13 @@ mod tests { .collect::>(); assert!(found.is_empty()); } + + #[test] + fn serialization_roundtrip() { + let magnet_url = std::fs::read_to_string("tests/bittorrent-v2-test.magnet").unwrap(); + let magnet = MagnetLink::new(&magnet_url).unwrap(); + let json_url = serde_json::to_string(&magnet_url).unwrap(); + let deserialized_magnet: MagnetLink = serde_json::from_str(&json_url).unwrap(); + assert_eq!(deserialized_magnet, magnet,); + } } From 2fa0245e0fb913d9a5016da9026bf84d26babd23 Mon Sep 17 00:00:00 2001 From: amateurforger Date: Mon, 19 Jan 2026 18:22:01 +0100 Subject: [PATCH 10/10] fix: Allow nullable Option --- Cargo.toml | 4 +- src/torrent_file.rs | 2 +- tests/sea_orm.rs | 242 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 241 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cdb0a97..90d99bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ rustc-hex = "2.1" serde = { version = "1", features = [ "derive" ] } fluent-uri = { version = "0.4", features = [ "serde" ] } # For Sea-ORM integration -sea-orm = { version = "2.0.0-rc.19", optional = true } -sea-orm-migration = { version = "2.0.0-rc.20", optional = true } +sea-orm = { version = "2.0.0-rc.38", optional = true } +sea-orm-migration = { version = "2.0.0-rc.38", optional = true } async-tempfile = { version = "0.7", optional = true } tokio = { version = "1", optional = true } diff --git a/src/torrent_file.rs b/src/torrent_file.rs index 98afac6..440cd29 100644 --- a/src/torrent_file.rs +++ b/src/torrent_file.rs @@ -345,7 +345,7 @@ impl sea_orm::TryGetable for TorrentFile { res: &sea_orm::QueryResult, index: I, ) -> Result { - let val: Vec = res.try_get_by(index)?; + let val = as sea_orm::TryGetable>::try_get_by(res, index)?; TorrentFile::from_slice(&val).map_err(|e| { sea_orm::error::TryGetError::DbErr(sea_orm::DbErr::TryIntoErr { from: "Bytes", diff --git a/tests/sea_orm.rs b/tests/sea_orm.rs index bbb4b6f..e6cc7cd 100644 --- a/tests/sea_orm.rs +++ b/tests/sea_orm.rs @@ -252,6 +252,76 @@ pub mod optional_mixed_magnet { } } +pub mod optional_mixed_torrent { + use super::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "optional_mixed_torrent")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub torrent_id: Option, + pub torrent: Option, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + #[async_trait::async_trait] + impl ActiveModelBehavior for ActiveModel {} + + pub mod migration { + use sea_orm_migration::prelude::*; + + pub mod m20251118_01_optional_mixed_torrent { + use sea_orm_migration::{prelude::*, schema::*}; + + #[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(OptionalMixedTorrent::Table) + .if_not_exists() + .col(pk_auto(OptionalMixedTorrent::Id)) + .col(string_null(OptionalMixedTorrent::TorrentID)) + .col(binary(OptionalMixedTorrent::Torrent).null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(OptionalMixedTorrent::Table).to_owned()) + .await + } + } + + #[derive(DeriveIden)] + enum OptionalMixedTorrent { + Table, + Id, + TorrentID, + Torrent, + } + } + + pub struct Migrator; + + #[async_trait::async_trait] + impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20251118_01_optional_mixed_torrent::Migration)] + } + } + } +} + #[test] fn test_magnet_active_model() { let magnet = @@ -289,7 +359,9 @@ async fn test_magnet_real_db() { let db = sea_orm::Database::connect(&format!("sqlite://{}?mode=rwc", sqlite_str)) .await .unwrap(); - mixed_magnet::Migrator::up(&db, None).await.unwrap(); + ::up(&db, None) + .await + .unwrap(); let magnet = MagnetLink::new(&std::fs::read_to_string("tests/bittorrent-v2-test.magnet").unwrap()) @@ -389,7 +461,7 @@ async fn test_torrent_real_optional_none() { let db = sea_orm::Database::connect(&format!("sqlite://{}?mode=rwc", sqlite_str)) .await .unwrap(); - optional_mixed_magnet::migration::Migrator::up(&db, None) + ::up(&db, None) .await .unwrap(); @@ -433,7 +505,7 @@ async fn test_torrent_real_optional_notset() { let db = sea_orm::Database::connect(&format!("sqlite://{}?mode=rwc", sqlite_str)) .await .unwrap(); - optional_mixed_magnet::migration::Migrator::up(&db, None) + ::up(&db, None) .await .unwrap(); @@ -477,7 +549,9 @@ async fn test_torrent_real_db() { let db = sea_orm::Database::connect(&format!("sqlite://{}?mode=rwc", sqlite_str)) .await .unwrap(); - mixed_torrent::Migrator::up(&db, None).await.unwrap(); + ::up(&db, None) + .await + .unwrap(); let torrent = TorrentFile::from_slice(&std::fs::read("tests/bittorrent-v2-test.torrent").unwrap()) @@ -564,3 +638,163 @@ async fn test_torrent_real_db() { assert!(found_one); assert!(found_two); } + +#[tokio::test] +async fn test_torrent_file_real_optional_none() { + use sea_orm_migration::*; + + let tmpdir = async_tempfile::TempDir::new().await.unwrap(); + let sqlite = tmpdir.join("optional_mixed_none.sqlite"); + let sqlite_str = sqlite.to_str().unwrap(); + + let db = sea_orm::Database::connect(&format!("sqlite://{}?mode=rwc", sqlite_str)) + .await + .unwrap(); + ::up( + &db, None, + ) + .await + .unwrap(); + + // Try with None + let model = optional_mixed_torrent::ActiveModel { + torrent_id: Set(None), + torrent: Set(None), + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let list = optional_mixed_torrent::Entity::find() + .all(&db) + .await + .unwrap(); + for entry in list { + println!("- {:?}", entry); + } + + let nonactive_model = model.try_into_model().unwrap(); + let saved_model = optional_mixed_torrent::Entity::find() + .filter(optional_mixed_torrent::Column::Torrent.is_null()) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model.torrent.as_ref(), None); + assert_eq!(nonactive_model.torrent.as_ref(), None); +} + +#[tokio::test] +async fn test_torrent_file_real_optional_notset() { + use sea_orm_migration::*; + + let tmpdir = async_tempfile::TempDir::new().await.unwrap(); + let sqlite = tmpdir.join("optional_mixed_notset.sqlite"); + let sqlite_str = sqlite.to_str().unwrap(); + + let db = sea_orm::Database::connect(&format!("sqlite://{}?mode=rwc", sqlite_str)) + .await + .unwrap(); + ::up( + &db, None, + ) + .await + .unwrap(); + + // Try with None + let model = optional_mixed_torrent::ActiveModel { + torrent_id: NotSet, + torrent: NotSet, + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let list = optional_mixed_torrent::Entity::find() + .all(&db) + .await + .unwrap(); + for entry in list { + println!("- {:?}", entry); + } + + let nonactive_model = model.try_into_model().unwrap(); + let saved_model = optional_mixed_torrent::Entity::find() + .filter(optional_mixed_torrent::Column::Torrent.is_null()) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!(saved_model.torrent.as_ref(), None); + assert_eq!(nonactive_model.torrent.as_ref(), None); +} + +#[tokio::test] +async fn test_torrent_file_real_optional_some() { + use sea_orm_migration::*; + + let torrent_file = + TorrentFile::from_slice(&std::fs::read("tests/bittorrent-v2-hybrid-test.torrent").unwrap()) + .unwrap(); + + let tmpdir = async_tempfile::TempDir::new().await.unwrap(); + let sqlite = tmpdir.join("optional_mixed_notset.sqlite"); + let sqlite_str = sqlite.to_str().unwrap(); + + let db = sea_orm::Database::connect(&format!("sqlite://{}?mode=rwc", sqlite_str)) + .await + .unwrap(); + ::up( + &db, None, + ) + .await + .unwrap(); + + let model_none = optional_mixed_torrent::ActiveModel { + torrent_id: NotSet, + torrent: NotSet, + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let model_some = optional_mixed_torrent::ActiveModel { + torrent_id: Set(Some(torrent_file.id())), + torrent: Set(Some(torrent_file.clone())), + ..Default::default() + } + .save(&db) + .await + .unwrap(); + + let nonactive_model_some = model_some.try_into_model().unwrap(); + let nonactive_model_none = model_none.try_into_model().unwrap(); + + let list = optional_mixed_torrent::Entity::find() + .all(&db) + .await + .unwrap(); + assert_eq!(list.len(), 2); + for entry in list { + println!("- {:?}", entry); + assert!(entry == nonactive_model_some || entry == nonactive_model_none); + } + + let saved_model_some = optional_mixed_torrent::Entity::find() + .filter(optional_mixed_torrent::Column::TorrentId.eq(torrent_file.id())) + .one(&db) + .await + .unwrap() + .unwrap(); + assert_eq!( + saved_model_some.torrent.as_ref(), + Some(torrent_file.clone()).as_ref() + ); + assert_eq!( + nonactive_model_some.torrent.as_ref(), + Some(torrent_file.clone()).as_ref() + ); +}