package mipt.monads
import cats.MonadThrow
import cats.data.OptionT
import cats.syntax.flatMap.toFlatMapOps
import cats.syntax.functor.toFunctorOps
import cats.syntax.traverse.toTraverseOps
import cats.syntax.applicativeError
import scala.concurrent.Future
import scala.util.control.NoStackTrace
trait UserRepository[F[_]]:
def findAll: F[List[User]]
def create(name: UserName, age: Age, friends: Set[UserId] = Set.empty): F[User]
def delete(userId: UserId): F[Unit]
def update(user: User): F[Unit]
object UserRepository:
case class UserNotFoundError(id: UserId) extends Throwable
type Op[F[_], T] = UserRepository[F] => F[T]
given [F[_]: MonadThrow]: MonadThrow[Op[F, *]] with
override def pure[A](a: A): Op[F, A] =
repo => MonadThrow[F].pure(a)
override def handleErrorWith[A](fa: Op[F, A])(f: Throwable => Op[F, A]): Op[F, A] =
repo => MonadThrow[F].handleErrorWith(fa(repo))(e => f(e)(repo))
override def raiseError[A](e: Throwable): Op[F, A] =
repo => MonadThrow[F].raiseError(e)
override def flatMap[A, B](fa: Op[F, A])(f: A => Op[F, B]): Op[F, B] =
repo => MonadThrow[F].flatMap(fa(repo))(a => f(a)(repo))
override def tailRecM[A, B](a: A)(f: A => Op[F, Either[A, B]]): Op[F, B] =
repo => MonadThrow[F].tailRecM(a)(a => f(a)(repo))
object Operations:
def findAll[F[_]]: Op[F, List[User]] =
_.findAll
def create[F[_]](name: UserName, age: Age, friends: Set[UserId] = Set.empty): Op[F, User] =
_.create(name, age, friends)
def delete[F[_]](userId: UserId): Op[F, Unit] = _.delete(userId)
def update[F[_]](user: User): Op[F, Unit] = _.update(user)
/** реализуйте композитные методы, используя базовые выше
*
* для работы с ошибками можно использовать синтаксис из cats.syntax.applicativeError val err: Op[User] =
* UserNotFoundError(UserId(1)).raiseError[Op, User]
*/
/** Метод опционального поиска пользователя */
def findMaybe[F[_]](userId: UserId)(using me: MonadThrow[F]): Op[F, Option[User]] =
repo => OptionT(repo.findAll.map(_.find(_.id == userId))).value
/** Метод поиска пользователя. Если пользователь не найден, должна генерироваться ошибка UserNotFound */
def find[F[_]](userId: UserId)(using me: MonadThrow[F]): Op[F, User] = repo =>
findMaybe[F](userId).map(_.getOrElse(throw UserNotFoundError(userId)))(repo)
/** Метод добавления друга к пользователю. */
def addFriend[F[_]](currentUserId: UserId, friendId: UserId)(using
me: MonadThrow[F]
): Op[F, User] = repo =>
for {
curUser <- find(currentUserId)(using me)(repo)
friend <- find(friendId)(using me)(repo)
curUserWithFriend = curUser.copy(friends = curUser.friends + friendId)
_ <- update(curUserWithFriend)(repo)
} yield curUserWithFriend
/** Метод удаления друга у пользователя */
def deleteFriend[F[_]](currentUserId: UserId, friendId: UserId)(using
me: MonadThrow[F]
): Op[F, User] = repo =>
for {
curUser <- find(currentUserId)(using me)(repo)
curUserWithoutFriend = curUser.copy(friends = curUser.friends - friendId)
_ <- update(curUserWithoutFriend)(repo)
} yield curUserWithoutFriend
/** Метод получения всех друзей пользователя */
def getUserFriends[F[_]](userId: UserId)(using
me: MonadThrow[F]
): Op[F, List[User]] = repo =>
for {
user <- find(userId)(using me)(repo)
friends <- user.friends.toList.traverse(find(_)(using me)(repo))
} yield friends
/** Метод получения пользователей, у которых в друзьях только взрослые пользователи */
def getUsersWithAdultOnlyFriends[F[_]]()(using
me: MonadThrow[F]
): Op[F, List[User]] = repo =>
repo.findAll.map(users =>
users.filter(u => u.friends.nonEmpty && u.friends.forall(fid => users.find(_.id == fid).get.isAdult))
)
/** Метод удаления всех молодых пользователей */
def deleteAllJuniorUsers[F[_]]()(using
me: MonadThrow[F]
): Op[F, Unit] = repo =>
for {
users <- repo.findAll
juniorUsers = users.filter(!_.isAdult)
_ <- juniorUsers.traverse(user => delete(user.id)(repo))
} yield ()
/** Метод создания сообщества, где все являются друзьями друг для друга. На вход подается список атрибутов
* пользователей из сообщества
*/
def createCommunity[F[_]](community: List[(UserName, Age)])(using
me: MonadThrow[F]
): Op[F, List[User]] = repo =>
for {
communityUsers <- community.traverse { case (name, age) => create(name, age)(repo) }
updatedCommunityUsers <- communityUsers.traverse { currentUser =>
val curUserWithFriend =
currentUser.copy(friends = communityUsers.filterNot(_ == currentUser).map(_.id).toSet)
update(curUserWithFriend)(repo).map(_ => curUserWithFriend)
}
} yield updatedCommunityUsers