ensemble 0.0.5

A Laravel-inspired ORM
Documentation
use inflector::Inflector;
use rbs::Value;
use serde::Serialize;
use std::{collections::HashMap, fmt::Debug};

use super::{find_related, Relationship, Status};
use crate::{
	query::Builder,
	value::{self, serializing_for_db},
	Error, Model,
};

/// ## A One to Many relationship.
/// A one-to-many relationship is used to define relationships where a single model is the parent to one or more child models.
/// For example, a blog post may have an infinite number of comments.
///
/// To define this relationship, we will place a comments field on the Post model. The comments field should be of type `HasMany<Post, Comment>`.
///
/// ## Example
///
/// ```rust
/// # use ensemble::{Model, relationships::HasMany};
/// # #[derive(Debug, Model)]
/// # struct Comment {
/// #   id: u64,
/// # }
/// #[derive(Debug, Model)]
/// struct Post {
///   id: u64,
///   title: String,
///   content: String,
///   comments: HasMany<Post, Comment>
/// }
///
/// # async fn call() -> Result<(), ensemble::Error> {
/// let mut post = Post::find(1).await?;
///
/// let comments: &Vec<Comment> = post.comments().await?;
/// # Ok(())
/// # }
/// ```
#[derive(Clone, Default)]
pub struct HasMany<Local: Model, Related: Model> {
	foreign_key: String,
	relation: Status<Vec<Related>>,
	/// The value of the local model's primary key.
	pub value: Local::PrimaryKey,
}

impl<Local: Model, Related: Model> Relationship for HasMany<Local, Related> {
	type Value = Vec<Related>;
	type Key = Local::PrimaryKey;
	type RelatedKey = Option<String>;

	fn build(value: Self::Key, foreign_key: Self::RelatedKey) -> Self {
		let foreign_key = foreign_key.unwrap_or_else(|| {
			format!("{}_{}", Local::NAME.to_snake_case(), Local::PRIMARY_KEY).to_snake_case()
		});

		Self {
			value,
			foreign_key,
			relation: Status::initial(),
		}
	}

	fn query(&self) -> Builder {
		Related::query()
			.r#where(
				&format!("{}.{}", Related::TABLE_NAME, self.foreign_key),
				"=",
				self.value.clone(),
			)
			.where_not_null(&format!("{}.{}", Related::TABLE_NAME, self.foreign_key))
	}

	fn peek(&self) -> Option<&Self::Value> {
		self.relation.as_ref()
	}

	/// Get the related models.
	///
	/// # Errors
	///
	/// Returns an error if the model cannot be retrieved, or if a connection to the database cannot be established.
	async fn get(&mut self) -> Result<&mut Self::Value, Error> {
		if self.relation.is_none() {
			let relation = self.query().get().await?;

			self.relation = Status::Fetched(Some(relation));
		}

		Ok(self.relation.as_mut().unwrap())
	}

	fn is_loaded(&self) -> bool {
		self.relation.is_loaded()
	}

	fn eager_query(&self, related: Vec<Self::Key>) -> Builder {
		Related::query()
			.r#where(
				&format!("{}.{}", Related::TABLE_NAME, self.foreign_key),
				"in",
				related,
			)
			.where_not_null(&format!("{}.{}", Related::TABLE_NAME, self.foreign_key))
	}

	fn r#match(&mut self, related: &[HashMap<String, Value>]) -> Result<(), Error> {
		let related = find_related(related, &self.foreign_key, &self.value, false)?;

		if !related.is_empty() {
			self.relation = Status::Fetched(Some(related));
		}

		Ok(())
	}
}

impl<Local: Model, Related: Model> HasMany<Local, Related> {
	/// Create a new `Related` model.
	///
	/// ## Errors
	///
	/// Returns an error if the model cannot be inserted, or if a connection to the database cannot be established.
	///
	/// ## Example
	///
	/// ```rust
	/// # use ensemble::{Model, relationships::HasMany};
	/// # #[derive(Debug, Model, Clone)]
	/// # struct Comment {
	/// #   id: u64,
	/// #   content: String,
	/// # }
	/// # #[derive(Debug, Model, Clone)]
	/// # struct Post {
	/// #  id: u64,
	/// #  comments: HasMany<Post, Comment>
	/// # }
	/// # async fn call() -> Result<(), ensemble::Error> {
	/// let mut post = Post::find(1).await?;
	///
	/// let comment = post.comments.create(Comment {
	///  id: 1,
	///  content: "Hello, world!".to_string(),
	/// }).await?;
	/// # Ok(())
	/// # }
	pub async fn create(&mut self, related: Related) -> Result<Related, Error>
	where
		Related: Clone,
	{
		let Value::Map(mut value) = rbs::to_value(related)? else {
			return Err(Error::Serialization(rbs::Error::Syntax(
				"Expected a map".to_string(),
			)));
		};

		value.insert(
			Value::String(self.foreign_key.clone()),
			value::for_db(&self.value)?,
		);

		let result = Related::create(rbs::from_value(Value::Map(value))?).await?;

		if let Status::Fetched(Some(relation)) = &mut self.relation {
			relation.push(result.clone());
		}

		Ok(result)
	}
}

impl<Local: Model, Related: Model> Debug for HasMany<Local, Related> {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		self.relation.fmt(f)
	}
}

impl<Local: Model, Related: Model> Serialize for HasMany<Local, Related> {
	fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
		if serializing_for_db::<S>() {
			if self.value == Default::default() {
				return serializer.serialize_none();
			}

			return self.value.serialize(serializer);
		}

		self.relation.serialize(serializer)
	}
}

#[cfg(feature = "schema")]
impl<Local: Model, Related: Model + schemars::JsonSchema> schemars::JsonSchema
	for HasMany<Local, Related>
{
	fn schema_name() -> String {
		<Option<Vec<Related>>>::schema_name()
	}

	fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
		gen.subschema_for::<Option<Vec<Related>>>()
	}
}