flix_db/entity/
info.rs

1//! This module contains entities for storing media information such as
2//! titles and overviews
3
4/// Collection entity
5pub mod collections {
6	use flix_model::id::CollectionId;
7
8	use sea_orm::entity::prelude::*;
9
10	/// The database representation of a flix collection
11	#[sea_orm::model]
12	#[derive(Debug, Clone, DeriveEntityModel)]
13	#[sea_orm(table_name = "flix_info_collections")]
14	pub struct Model {
15		/// The collection's ID
16		#[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)]
17		pub id: CollectionId,
18		/// The collection's title
19		#[sea_orm(indexed)]
20		pub title: String,
21		/// The collection's overview
22		pub overview: String,
23	}
24
25	impl ActiveModelBehavior for ActiveModel {}
26}
27
28/// Movie entity
29pub mod movies {
30	use flix_model::id::MovieId;
31
32	use chrono::NaiveDate;
33	use sea_orm::entity::prelude::*;
34
35	/// The database representation of a flix movie
36	#[sea_orm::model]
37	#[derive(Debug, Clone, DeriveEntityModel)]
38	#[sea_orm(table_name = "flix_info_movies")]
39	pub struct Model {
40		/// The movie's ID
41		#[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)]
42		pub id: MovieId,
43		/// The movie's title
44		#[sea_orm(indexed)]
45		pub title: String,
46		/// The movie's tagline
47		pub tagline: String,
48		/// The movie's overview
49		pub overview: String,
50		/// The movie's release date
51		#[sea_orm(indexed)]
52		pub date: NaiveDate,
53	}
54
55	impl ActiveModelBehavior for ActiveModel {}
56}
57
58/// Show entity
59pub mod shows {
60	use flix_model::id::ShowId;
61
62	use chrono::NaiveDate;
63	use sea_orm::entity::prelude::*;
64
65	/// The database representation of a flix show
66	#[sea_orm::model]
67	#[derive(Debug, Clone, DeriveEntityModel)]
68	#[sea_orm(table_name = "flix_info_shows")]
69	pub struct Model {
70		/// The show's ID
71		#[sea_orm(column_type = "Integer", primary_key, nullable, auto_increment = false)]
72		pub id: ShowId,
73		/// The show's title
74		#[sea_orm(indexed)]
75		pub title: String,
76		/// The show's tagline
77		pub tagline: String,
78		/// The show's overview
79		pub overview: String,
80		/// The show's air date
81		#[sea_orm(indexed)]
82		pub date: NaiveDate,
83
84		/// Seasons that are part of this show
85		#[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")]
86		pub seasons: HasMany<super::seasons::Entity>,
87		/// Episodes that are part of this show
88		#[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")]
89		pub episodes: HasMany<super::episodes::Entity>,
90	}
91
92	impl ActiveModelBehavior for ActiveModel {}
93}
94
95/// Season entity
96pub mod seasons {
97	use flix_model::id::ShowId;
98	use flix_model::numbers::SeasonNumber;
99
100	use chrono::NaiveDate;
101	use sea_orm::entity::prelude::*;
102
103	/// The database representation of a flix season
104	#[sea_orm::model]
105	#[derive(Debug, Clone, DeriveEntityModel)]
106	#[sea_orm(table_name = "flix_info_seasons")]
107	pub struct Model {
108		/// The season's show's ID
109		#[sea_orm(primary_key, auto_increment = false)]
110		pub show_id: ShowId,
111		/// The season's number
112		#[sea_orm(primary_key, auto_increment = false)]
113		pub season_number: SeasonNumber,
114		/// The season's title
115		pub title: String,
116		/// The season's overview
117		pub overview: String,
118		/// The season's air date
119		#[sea_orm(indexed)]
120		pub date: NaiveDate,
121
122		/// The show this season belongs to
123		#[sea_orm(belongs_to, from = "show_id", to = "id")]
124		pub show: HasOne<super::shows::Entity>,
125		/// Episodes that are part of this season
126		#[sea_orm(has_many, on_update = "Cascade", on_delete = "Cascade")]
127		pub episodes: HasMany<super::episodes::Entity>,
128	}
129
130	impl ActiveModelBehavior for ActiveModel {}
131}
132
133/// Episode entity
134pub mod episodes {
135	use flix_model::id::ShowId;
136	use flix_model::numbers::{EpisodeNumber, SeasonNumber};
137
138	use chrono::NaiveDate;
139	use sea_orm::entity::prelude::*;
140
141	/// The database representation of a flix episode
142	#[sea_orm::model]
143	#[derive(Debug, Clone, DeriveEntityModel)]
144	#[sea_orm(table_name = "flix_info_episodes")]
145	pub struct Model {
146		/// The episode's show's ID
147		#[sea_orm(primary_key, auto_increment = false)]
148		pub show_id: ShowId,
149		/// The episode's season's number
150		#[sea_orm(primary_key, auto_increment = false)]
151		pub season_number: SeasonNumber,
152		/// The episode's number
153		#[sea_orm(primary_key, auto_increment = false)]
154		pub episode_number: EpisodeNumber,
155		/// The episode's title
156		pub title: String,
157		/// The episode's overview
158		pub overview: String,
159		/// The episode's air date
160		#[sea_orm(indexed)]
161		pub date: NaiveDate,
162
163		/// The show this episode belongs to
164		#[sea_orm(belongs_to, from = "show_id", to = "id")]
165		pub show: HasOne<super::shows::Entity>,
166		/// The season this episode belongs to
167		#[sea_orm(
168			belongs_to,
169			from = "(show_id, season_number)",
170			to = "(show_id, season_number)"
171		)]
172		pub season: HasOne<super::seasons::Entity>,
173	}
174
175	impl ActiveModelBehavior for ActiveModel {}
176}
177
178/// Macros for creating info entities
179#[cfg(test)]
180pub mod test {
181	macro_rules! make_info_collection {
182		($db:expr, $id:literal) => {
183			$crate::entity::info::collections::ActiveModel {
184				id: Set(::flix_model::id::CollectionId::from_raw($id)),
185				title: Set(::std::string::String::new()),
186				overview: Set(::std::string::String::new()),
187			}
188			.insert($db)
189			.await
190			.expect("insert");
191		};
192	}
193	pub(crate) use make_info_collection;
194
195	macro_rules! make_info_movie {
196		($db:expr, $id:literal) => {
197			$crate::entity::info::movies::ActiveModel {
198				id: Set(::flix_model::id::MovieId::from_raw($id)),
199				title: Set(::std::string::String::new()),
200				tagline: Set(::std::string::String::new()),
201				overview: Set(::std::string::String::new()),
202				date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")),
203			}
204			.insert($db)
205			.await
206			.expect("insert");
207		};
208	}
209	pub(crate) use make_info_movie;
210
211	macro_rules! make_info_show {
212		($db:expr, $id:literal) => {
213			$crate::entity::info::shows::ActiveModel {
214				id: Set(::flix_model::id::ShowId::from_raw($id)),
215				title: Set(::std::string::String::new()),
216				tagline: Set(::std::string::String::new()),
217				overview: Set(::std::string::String::new()),
218				date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")),
219			}
220			.insert($db)
221			.await
222			.expect("insert");
223		};
224	}
225	pub(crate) use make_info_show;
226
227	macro_rules! make_info_season {
228		($db:expr, $show:literal, $season:literal) => {
229			$crate::entity::info::seasons::ActiveModel {
230				show_id: Set(::flix_model::id::ShowId::from_raw($show)),
231				season_number: Set($season),
232				title: Set(::std::string::String::new()),
233				overview: Set(::std::string::String::new()),
234				date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")),
235			}
236			.insert($db)
237			.await
238			.expect("insert");
239		};
240	}
241	pub(crate) use make_info_season;
242
243	macro_rules! make_info_episode {
244		($db:expr, $show:literal, $season:literal, $episode:literal) => {
245			$crate::entity::info::episodes::ActiveModel {
246				show_id: Set(::flix_model::id::ShowId::from_raw($show)),
247				season_number: Set($season),
248				episode_number: Set($episode),
249				title: Set(::std::string::String::new()),
250				overview: Set(::std::string::String::new()),
251				date: Set(::chrono::NaiveDate::from_yo_opt(1, 1).expect("from_yo_opt")),
252			}
253			.insert($db)
254			.await
255			.expect("insert");
256		};
257	}
258	pub(crate) use make_info_episode;
259}
260
261#[cfg(test)]
262mod tests {
263	use flix_model::id::{CollectionId, MovieId, ShowId};
264
265	use chrono::NaiveDate;
266	use sea_orm::ActiveValue::{NotSet, Set};
267	use sea_orm::entity::prelude::*;
268	use sea_orm::sqlx::error::ErrorKind;
269
270	use crate::tests::new_initialized_memory_db;
271
272	use super::super::tests::get_error_kind;
273	use super::super::tests::notsettable;
274	use super::test::{
275		make_info_collection, make_info_episode, make_info_movie, make_info_season, make_info_show,
276	};
277
278	#[tokio::test]
279	async fn use_test_macros() {
280		let db = new_initialized_memory_db().await;
281
282		make_info_collection!(&db, 1);
283		make_info_movie!(&db, 1);
284		make_info_show!(&db, 1);
285		make_info_season!(&db, 1, 1);
286		make_info_episode!(&db, 1, 1, 1);
287	}
288
289	#[tokio::test]
290	async fn test_round_trip_collections() {
291		let db = new_initialized_memory_db().await;
292
293		macro_rules! assert_collection {
294			($db:expr, $id:literal, Success $(; $($skip:ident),+)?) => {
295				let model = assert_collection!(@insert, $db, $id $(; $($skip),+)?)
296					.expect("insert");
297
298				assert_eq!(model.id, CollectionId::from_raw($id));
299				assert_eq!(model.title, concat!("C Title ", $id));
300				assert_eq!(model.overview, concat!("C Overview ", $id));
301			};
302			($db:expr, $id:literal, $error:ident $(; $($skip:ident),+)?) => {
303				let model = assert_collection!(@insert, $db, $id $(; $($skip),+)?)
304					.expect_err("insert");
305
306				assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
307			};
308			(@insert, $db:expr, $id:literal $(; $($skip:ident),+)?) => {
309				super::collections::ActiveModel {
310					id: notsettable!(id, CollectionId::from_raw($id) $(, $($skip),+)?),
311					title: notsettable!(title, concat!("C Title ", $id).to_string() $(, $($skip),+)?),
312					overview: notsettable!(overview, concat!("C Overview ", $id).to_string() $(, $($skip),+)?),
313				}.insert($db).await
314			};
315		}
316
317		assert_collection!(&db, 1, Success);
318		assert_collection!(&db, 1, UniqueViolation);
319		assert_collection!(&db, 2, Success);
320
321		assert_collection!(&db, 3, Success; id);
322		assert_collection!(&db, 4, NotNullViolation; title);
323		assert_collection!(&db, 5, NotNullViolation; overview);
324	}
325
326	#[tokio::test]
327	async fn test_round_trip_movies() {
328		let db = new_initialized_memory_db().await;
329
330		macro_rules! assert_movie {
331			($db:expr, $id:literal, Success $(; $($skip:ident),+)?) => {
332				let model = assert_movie!(@insert, $db, $id $(; $($skip),+)?)
333					.expect("insert");
334
335				assert_eq!(model.id, MovieId::from_raw($id));
336				assert_eq!(model.title, concat!("M Title ", $id));
337				assert_eq!(model.tagline, concat!("M Tagline ", $id));
338				assert_eq!(model.overview, concat!("M Overview ", $id));
339				assert_eq!(model.date, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt"));
340			};
341			($db:expr, $id:literal, $error:ident $(; $($skip:ident),+)?) => {
342				let model = assert_movie!(@insert, $db, $id $(; $($skip),+)?)
343					.expect_err("insert");
344
345				assert_eq!(get_error_kind(model).expect("get_error_kind"), ErrorKind::$error);
346			};
347			(@insert, $db:expr, $id:literal $(; $($skip:ident),+)?) => {
348				super::movies::ActiveModel {
349					id: notsettable!(id, MovieId::from_raw($id) $(, $($skip),+)?),
350					title: notsettable!(title, concat!("M Title ", $id).to_string() $(, $($skip),+)?),
351					tagline: notsettable!(tagline, concat!("M Tagline ", $id).to_string() $(, $($skip),+)?),
352					overview: notsettable!(overview, concat!("M Overview ", $id).to_string() $(, $($skip),+)?),
353					date: notsettable!(date, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt") $(, $($skip),+)?),
354				}.insert($db).await
355			};
356		}
357
358		assert_movie!(&db, 1, Success);
359		assert_movie!(&db, 1, UniqueViolation);
360		assert_movie!(&db, 2, Success);
361
362		assert_movie!(&db, 3, Success; id);
363		assert_movie!(&db, 4, NotNullViolation; title);
364		assert_movie!(&db, 5, NotNullViolation; tagline);
365		assert_movie!(&db, 6, NotNullViolation; overview);
366		assert_movie!(&db, 7, NotNullViolation; date);
367	}
368
369	#[tokio::test]
370	async fn test_round_trip_shows() {
371		let db = new_initialized_memory_db().await;
372
373		macro_rules! assert_show {
374			($db:expr, $id:literal, Success $(; $($skip:ident),+)?) => {
375				let model = assert_show!(@insert, $db, $id $(; $($skip),+)?)
376					.expect("insert");
377
378				assert_eq!(model.id, ShowId::from_raw($id));
379				assert_eq!(model.title, concat!("S Title ", $id));
380				assert_eq!(model.tagline, concat!("S Tagline ", $id));
381				assert_eq!(model.overview, concat!("S Overview ", $id));
382				assert_eq!(model.date, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt"));
383			};
384			($db:expr, $id:literal, $error:ident $(; $($skip:ident),+)?) => {
385				let model = assert_show!(@insert, $db, $id $(; $($skip),+)?)
386					.expect_err("insert");
387
388				assert_eq!(
389					get_error_kind(model).expect("get_error_kind"),
390					ErrorKind::$error
391				);
392			};
393			(@insert, $db:expr, $id:literal $(; $($skip:ident),+)?) => {
394				super::shows::ActiveModel {
395					id: notsettable!(id, ShowId::from_raw($id) $(, $($skip),+)?),
396					title: notsettable!(title, concat!("S Title ", $id).to_string() $(, $($skip),+)?),
397					tagline: notsettable!(tagline, concat!("S Tagline ", $id).to_string() $(, $($skip),+)?),
398					overview: notsettable!(overview, concat!("S Overview ", $id).to_string() $(, $($skip),+)?),
399					date: notsettable!(date, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt") $(, $($skip),+)?),
400				}.insert($db).await
401			};
402		}
403
404		assert_show!(&db, 1, Success);
405		assert_show!(&db, 1, UniqueViolation);
406		assert_show!(&db, 2, Success);
407
408		assert_show!(&db, 3, Success; id);
409		assert_show!(&db, 4, NotNullViolation; title);
410		assert_show!(&db, 5, NotNullViolation; tagline);
411		assert_show!(&db, 6, NotNullViolation; overview);
412		assert_show!(&db, 7, NotNullViolation; date);
413	}
414
415	#[tokio::test]
416	async fn test_round_trip_seasons() {
417		let db = new_initialized_memory_db().await;
418
419		macro_rules! assert_season {
420			($db:expr, $show:literal, $season:literal, Success $(; $($skip:ident),+)?) => {
421				let model = assert_season!(@insert, $db, $show, $season $(; $($skip),+)?)
422					.expect("insert");
423
424				assert_eq!(model.show_id, ShowId::from_raw($show));
425				assert_eq!(model.season_number, $season);
426				assert_eq!(model.title, concat!("SS Title ", $show, ",", $season));
427				assert_eq!(model.overview, concat!("SS Overview ", $show, ",", $season));
428				assert_eq!(model.date, NaiveDate::from_yo_opt($show + $season, 1).expect("from_yo_opt"));
429			};
430			($db:expr, $show:literal, $season:literal, $error:ident $(; $($skip:ident),+)?) => {
431				let model = assert_season!(@insert, $db, $show, $season $(; $($skip),+)?)
432					.expect_err("insert");
433
434				assert_eq!(
435					get_error_kind(model).expect("get_error_kind"),
436					ErrorKind::$error
437				);
438			};
439			(@insert, $db:expr, $show:literal, $season:literal $(; $($skip:ident),+)?) => {
440				super::seasons::ActiveModel {
441					show_id: notsettable!(show_id, ShowId::from_raw($show) $(, $($skip),+)?),
442					season_number: notsettable!(season_number, $season $(, $($skip),+)?),
443					title: notsettable!(title, concat!("SS Title ", $show, ",", $season).to_string() $(, $($skip),+)?),
444					overview: notsettable!(overview, concat!("SS Overview ", $show, ",", $season).to_string() $(, $($skip),+)?),
445					date: notsettable!(date, NaiveDate::from_yo_opt($show + $season, 1).expect("from_yo_opt") $(, $($skip),+)?),
446				}.insert($db).await
447			};
448		}
449
450		assert_season!(&db, 1, 1, ForeignKeyViolation);
451		make_info_show!(&db, 1);
452		make_info_show!(&db, 2);
453
454		assert_season!(&db, 1, 1, Success);
455		assert_season!(&db, 1, 1, UniqueViolation);
456		assert_season!(&db, 2, 1, Success);
457		assert_season!(&db, 1, 2, Success);
458
459		assert_season!(&db, 1, 3, NotNullViolation; show_id);
460		assert_season!(&db, 1, 4, NotNullViolation; season_number);
461		assert_season!(&db, 1, 5, NotNullViolation; title);
462		assert_season!(&db, 1, 6, NotNullViolation; overview);
463		assert_season!(&db, 1, 7, NotNullViolation; date);
464	}
465
466	#[tokio::test]
467	async fn test_round_trip_episodes() {
468		let db = new_initialized_memory_db().await;
469
470		macro_rules! assert_episode {
471			($db:expr, $show:literal, $season:literal, $episode:literal, Success $(; $($skip:ident),+)?) => {
472				let model = assert_episode!(@insert, $db, $show, $season, $episode $(; $($skip),+)?)
473					.expect("insert");
474
475				assert_eq!(model.show_id, ShowId::from_raw($show));
476				assert_eq!(model.season_number, $season);
477				assert_eq!(model.episode_number, $episode);
478				assert_eq!(model.title, concat!("SSE Title ", $show, ",", $season, ",", $episode));
479				assert_eq!(model.overview, concat!("SSE Overview ", $show, ",", $season, ",", $episode));
480				assert_eq!(model.date, NaiveDate::from_yo_opt($show + $season, 1).expect("from_yo_opt"));
481			};
482			($db:expr, $show:literal, $season:literal, $episode:literal, $error:ident $(; $($skip:ident),+)?) => {
483				let model = assert_episode!(@insert, $db, $show, $season, $episode $(; $($skip),+)?)
484					.expect_err("insert");
485
486				assert_eq!(
487					get_error_kind(model).expect("get_error_kind"),
488					ErrorKind::$error
489				);
490			};
491			(@insert, $db:expr, $show:literal, $season:literal, $episode:literal $(; $($skip:ident),+)?) => {
492				super::episodes::ActiveModel {
493					show_id: notsettable!(show_id, ShowId::from_raw($show) $(, $($skip),+)?),
494					season_number: notsettable!(season_number, $season $(, $($skip),+)?),
495					episode_number: notsettable!(episode_number, $episode $(, $($skip),+)?),
496					title: notsettable!(title, concat!("SSE Title ", $show, ",", $season, ",", $episode).to_string() $(, $($skip),+)?),
497					overview: notsettable!(overview, concat!("SSE Overview ", $show, ",", $season, ",", $episode).to_string() $(, $($skip),+)?),
498					date: notsettable!(date, NaiveDate::from_yo_opt($show + $season, 1).expect("from_yo_opt") $(, $($skip),+)?),
499				}.insert($db).await
500			};
501		}
502
503		assert_episode!(&db, 1, 1, 1, ForeignKeyViolation);
504		make_info_show!(&db, 1);
505		make_info_show!(&db, 2);
506		assert_episode!(&db, 1, 1, 1, ForeignKeyViolation);
507		make_info_season!(&db, 1, 1);
508		make_info_season!(&db, 1, 2);
509		make_info_season!(&db, 2, 1);
510
511		assert_episode!(&db, 1, 1, 1, Success);
512		assert_episode!(&db, 1, 1, 1, UniqueViolation);
513		assert_episode!(&db, 2, 1, 1, Success);
514		assert_episode!(&db, 1, 2, 1, Success);
515		assert_episode!(&db, 1, 1, 2, Success);
516
517		assert_episode!(&db, 1, 1, 3, NotNullViolation; show_id);
518		assert_episode!(&db, 1, 1, 4, NotNullViolation; season_number);
519		assert_episode!(&db, 1, 1, 4, NotNullViolation; episode_number);
520		assert_episode!(&db, 1, 1, 5, NotNullViolation; title);
521		assert_episode!(&db, 1, 1, 6, NotNullViolation; overview);
522		assert_episode!(&db, 1, 1, 7, NotNullViolation; date);
523	}
524}