From 4f04d27eaada9db285e7176367204b57d7032820 Mon Sep 17 00:00:00 2001 From: Isto Nikula Date: Sat, 12 Jan 2019 19:02:28 +0200 Subject: [PATCH 01/10] refactor(feeds): use MonadDefer instead of IO in domain --- .../src/main/kotlin/io/realworld/Application.kt | 4 +++- .../main/kotlin/io/realworld/articles/Routes.kt | 14 +++++++++----- .../test/kotlin/io/realworld/CommentTests.kt | 3 ++- .../test/kotlin/io/realworld/FavoriteTests.kt | 3 ++- .../realworld/domain/articles/Dependencies.kt | 5 +++-- .../io/realworld/domain/articles/UseCases.kt | 17 +++++++++-------- .../realworld/persistence/ArticleRepository.kt | 17 ++++++++++------- 7 files changed, 38 insertions(+), 25 deletions(-) diff --git a/realworld-app/web/src/main/kotlin/io/realworld/Application.kt b/realworld-app/web/src/main/kotlin/io/realworld/Application.kt index 9d9a125..6bd1ef2 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/Application.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/Application.kt @@ -1,6 +1,8 @@ package io.realworld import arrow.core.getOrElse +import arrow.effects.IO +import arrow.effects.instances.io.monadDefer.monadDefer import io.realworld.domain.common.Auth import io.realworld.domain.common.Settings import io.realworld.domain.common.Token @@ -50,7 +52,7 @@ class Application : WebMvcConfigurer { @Bean fun articleRepository(jdbcTemplate: NamedParameterJdbcTemplate, userRepository: UserRepository) = ArticleRepository( - jdbcTemplate, userRepository + jdbcTemplate, userRepository, IO.monadDefer() ) override fun addArgumentResolvers(resolvers: MutableList) { diff --git a/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt b/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt index 4e8a739..4265c9a 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt @@ -1,5 +1,9 @@ package io.realworld.articles +import arrow.effects.ForIO +import arrow.effects.IO +import arrow.effects.fix +import arrow.effects.instances.io.monadDefer.monadDefer import io.realworld.ForbiddenException import io.realworld.JwtTokenResolver import io.realworld.authHeader @@ -31,7 +35,7 @@ import io.realworld.domain.articles.GetArticlesUseCase import io.realworld.domain.articles.GetCommentsCommand import io.realworld.domain.articles.GetCommentsUseCase import io.realworld.domain.articles.GetFeedsCommand -import io.realworld.domain.articles.GetFeedsUsecase +import io.realworld.domain.articles.GetFeedsUseCase import io.realworld.domain.articles.GetTagsCommand import io.realworld.domain.articles.GetTagsUseCase import io.realworld.domain.articles.UnfavoriteArticleCommand @@ -96,7 +100,7 @@ data class TagsResponse(val tags: Set) @RestController class ArticleController( private val auth: Auth, - private val articleRepo: ArticleRepository, + private val articleRepo: ArticleRepository, private val userRepo: UserRepository, private val txManager: PlatformTransactionManager ) { @@ -146,12 +150,12 @@ class ArticleController( filter: FeedFilter, user: User ): ResponseEntity { - return object : GetFeedsUsecase { + return object : GetFeedsUseCase { override val getFeeds = articleRepo::getFeeds override val getFeedsCount = articleRepo::getFeedsCount }.run { - GetFeedsCommand(filter, user).runUseCase() - }.runReadTx(txManager).let { + GetFeedsCommand(filter, user).runUseCase(IO.monadDefer()) + }.fix().runReadTx(txManager).let { ResponseEntity.ok(ArticlesResponse.fromDomain(it)) } } diff --git a/realworld-app/web/src/test/kotlin/io/realworld/CommentTests.kt b/realworld-app/web/src/test/kotlin/io/realworld/CommentTests.kt index dd6524d..dc92d1d 100644 --- a/realworld-app/web/src/test/kotlin/io/realworld/CommentTests.kt +++ b/realworld-app/web/src/test/kotlin/io/realworld/CommentTests.kt @@ -1,5 +1,6 @@ package io.realworld +import arrow.effects.ForIO import io.realworld.articles.CommentDto import io.realworld.articles.CommentResponse import io.realworld.domain.common.Auth @@ -43,7 +44,7 @@ class CommentTests { lateinit var jdbcTemplate: JdbcTemplate @Autowired - lateinit var articleRepo: ArticleRepository + lateinit var articleRepo: ArticleRepository @Autowired lateinit var auth: Auth diff --git a/realworld-app/web/src/test/kotlin/io/realworld/FavoriteTests.kt b/realworld-app/web/src/test/kotlin/io/realworld/FavoriteTests.kt index 23ac9c0..37bbdb5 100644 --- a/realworld-app/web/src/test/kotlin/io/realworld/FavoriteTests.kt +++ b/realworld-app/web/src/test/kotlin/io/realworld/FavoriteTests.kt @@ -1,5 +1,6 @@ package io.realworld +import arrow.effects.ForIO import io.realworld.articles.UpdateDto import io.realworld.domain.common.Auth import io.realworld.persistence.ArticleRepository @@ -34,7 +35,7 @@ class FavoriteTests { lateinit var jdbcTemplate: JdbcTemplate @Autowired - lateinit var articleRepo: ArticleRepository + lateinit var articleRepo: ArticleRepository @Autowired lateinit var auth: Auth diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/articles/Dependencies.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/articles/Dependencies.kt index 24f0474..dbb8a72 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/articles/Dependencies.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/articles/Dependencies.kt @@ -1,5 +1,6 @@ package io.realworld.domain.articles +import arrow.Kind import arrow.core.Either import arrow.core.Option import arrow.effects.IO @@ -17,8 +18,8 @@ typealias ExistsBySlug = (slug: String) -> IO typealias GetArticleBySlug = (slug: String, Option) -> IO> typealias GetArticles = (ArticleFilter, Option) -> IO> typealias GetArticlesCount = (ArticleFilter) -> IO -typealias GetFeeds = (FeedFilter, User) -> IO> -typealias GetFeedsCount = (user: User) -> IO +typealias GetFeeds = (FeedFilter, User) -> Kind> +typealias GetFeedsCount = (user: User) -> Kind typealias DeleteArticle = (ArticleId) -> IO diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt index b257736..0b11e19 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt @@ -1,5 +1,6 @@ package io.realworld.domain.articles +import arrow.Kind import arrow.core.Either import arrow.core.Option import arrow.core.getOrElse @@ -13,6 +14,7 @@ import arrow.effects.ForIO import arrow.effects.IO import arrow.effects.fix import arrow.effects.instances.io.monad.monad +import arrow.effects.typeclasses.MonadDefer import arrow.instances.monad import arrow.typeclasses.binding import io.realworld.domain.users.User @@ -126,21 +128,20 @@ interface GetArticlesUseCase { } } -interface GetFeedsUsecase { - val getFeeds: GetFeeds - val getFeedsCount: GetFeedsCount +interface GetFeedsUseCase { + val getFeeds: GetFeeds + val getFeedsCount: GetFeedsCount - fun GetFeedsCommand.runUseCase(): IO, Long>> { + fun GetFeedsCommand.runUseCase(MD: MonadDefer): Kind, Long>> { val cmd = this - return IO.monad().binding { + return MD.binding { val count = getFeedsCount(cmd.user).bind() - if (count == 0L) - Pair(listOf(), 0L) + if (count == 0L) Pair(listOf(), 0L) else { val feeds = getFeeds(cmd.filter, cmd.user).bind() Pair(feeds, count) } - }.fix() + } } } diff --git a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt index 31634c3..7bb7686 100644 --- a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt +++ b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt @@ -1,10 +1,12 @@ package io.realworld.persistence +import arrow.Kind import arrow.core.Option import arrow.core.getOrElse import arrow.core.some import arrow.core.toOption import arrow.effects.IO +import arrow.effects.typeclasses.MonadDefer import io.realworld.domain.articles.Article import io.realworld.domain.articles.ArticleFilter import io.realworld.domain.articles.ArticleId @@ -106,10 +108,11 @@ private fun Comment.Companion.from(row: CommentRow, deps: CommentDeps) = Comment author = deps.author ) -class ArticleRepository( +class ArticleRepository( val jdbcTemplate: NamedParameterJdbcTemplate, - val userRepo: UserRepository -) { + val userRepo: UserRepository, + MD: MonadDefer +) : MonadDefer by MD { fun create(article: ValidArticleCreation, user: User): IO
= IO { val row = insertArticleRow(article, user) @@ -208,14 +211,14 @@ class ArticleRepository( fetchArticleRowCount(filter.toQueryParts()) } - fun getFeeds(filter: FeedFilter, user: User): IO> = IO { + fun getFeeds(filter: FeedFilter, user: User): Kind> = defer { val rows = fetchArticleRows(user.toFeedsQueryParts(), filter.limit, filter.offset) // NOTE: opt for simplicity (query limit defaults to 20), thus let's loop - rows.map { row -> Article.from(row, loadArticleDeps(row, user.some())) } + rows.map { row -> Article.from(row, loadArticleDeps(row, user.some())) }.just() } - fun getFeedsCount(user: User): IO = IO { - fetchArticleRowCount(user.toFeedsQueryParts()) + fun getFeedsCount(user: User): Kind = defer { + fetchArticleRowCount(user.toFeedsQueryParts()).just() } fun getTags(): IO> = with(TagTbl) { From f8edfa72dfbcf1f65b0f5e8938de30de07b16c7c Mon Sep 17 00:00:00 2001 From: Isto Nikula Date: Sat, 12 Jan 2019 19:23:54 +0200 Subject: [PATCH 02/10] refactor(user-repo): prepare for IO removal --- .../web/src/main/kotlin/io/realworld/Application.kt | 10 +++++++--- .../src/main/kotlin/io/realworld/articles/Routes.kt | 2 +- .../src/main/kotlin/io/realworld/profiles/Routes.kt | 3 ++- .../web/src/main/kotlin/io/realworld/users/Routes.kt | 3 ++- .../web/src/test/kotlin/io/realworld/ArticleTests.kt | 3 ++- .../web/src/test/kotlin/io/realworld/CommentTests.kt | 2 +- .../web/src/test/kotlin/io/realworld/FavoriteTests.kt | 2 +- .../web/src/test/kotlin/io/realworld/ProfileTests.kt | 3 ++- .../web/src/test/kotlin/io/realworld/UserTests.kt | 3 ++- .../io/realworld/persistence/ArticleRepository.kt | 2 +- .../kotlin/io/realworld/persistence/UserRepository.kt | 6 +++++- 11 files changed, 26 insertions(+), 13 deletions(-) diff --git a/realworld-app/web/src/main/kotlin/io/realworld/Application.kt b/realworld-app/web/src/main/kotlin/io/realworld/Application.kt index 6bd1ef2..dd66e6e 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/Application.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/Application.kt @@ -1,6 +1,7 @@ package io.realworld import arrow.core.getOrElse +import arrow.effects.ForIO import arrow.effects.IO import arrow.effects.instances.io.monadDefer.monadDefer import io.realworld.domain.common.Auth @@ -37,7 +38,7 @@ class Application : WebMvcConfigurer { @Bean fun userCreator() = object : (Token) -> User { @Autowired - lateinit var repo: UserRepository + lateinit var repo: UserRepository override fun invoke(token: Token): User { return repo.findById(token.id).unsafeRunSync().map { it.user }.getOrElse { throw UnauthorizedException() } @@ -48,10 +49,13 @@ class Application : WebMvcConfigurer { fun auth() = Auth(settings().security) @Bean - fun userRepository(jdbcTemplate: NamedParameterJdbcTemplate) = UserRepository(jdbcTemplate) + fun userRepository(jdbcTemplate: NamedParameterJdbcTemplate) = UserRepository(jdbcTemplate, IO.monadDefer()) @Bean - fun articleRepository(jdbcTemplate: NamedParameterJdbcTemplate, userRepository: UserRepository) = ArticleRepository( + fun articleRepository( + jdbcTemplate: NamedParameterJdbcTemplate, + userRepository: UserRepository + ) = ArticleRepository( jdbcTemplate, userRepository, IO.monadDefer() ) diff --git a/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt b/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt index 4265c9a..5487792 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt @@ -101,7 +101,7 @@ data class TagsResponse(val tags: Set) class ArticleController( private val auth: Auth, private val articleRepo: ArticleRepository, - private val userRepo: UserRepository, + private val userRepo: UserRepository, private val txManager: PlatformTransactionManager ) { @PostMapping("/api/articles") diff --git a/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt b/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt index 75974a5..2606cd3 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt @@ -1,5 +1,6 @@ package io.realworld.profiles +import arrow.effects.ForIO import io.realworld.JwtTokenResolver import io.realworld.authHeader import io.realworld.domain.common.Auth @@ -32,7 +33,7 @@ data class ProfileResponse(val profile: ProfileResponseDto) { @RestController class ProfileController( private val auth: Auth, - private val repo: UserRepository, + private val repo: UserRepository, private val txManager: PlatformTransactionManager ) { diff --git a/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt b/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt index e5eee13..645f62c 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt @@ -1,6 +1,7 @@ package io.realworld.users import arrow.core.Option +import arrow.effects.ForIO import io.realworld.FieldError import io.realworld.UnauthorizedException import io.realworld.domain.common.Auth @@ -43,7 +44,7 @@ data class UserResponse(val user: UserResponseDto) { @RestController class UserController( private val auth0: Auth, - private val repo: UserRepository, + private val repo: UserRepository, private val txManager: PlatformTransactionManager ) { diff --git a/realworld-app/web/src/test/kotlin/io/realworld/ArticleTests.kt b/realworld-app/web/src/test/kotlin/io/realworld/ArticleTests.kt index b7cb74f..4f8dfb6 100644 --- a/realworld-app/web/src/test/kotlin/io/realworld/ArticleTests.kt +++ b/realworld-app/web/src/test/kotlin/io/realworld/ArticleTests.kt @@ -1,5 +1,6 @@ package io.realworld +import arrow.effects.ForIO import io.realworld.articles.ArticleResponse import io.realworld.articles.ArticleResponseDto import io.realworld.articles.ArticlesResponse @@ -181,7 +182,7 @@ class ArticleTests { lateinit var auth: Auth @Autowired - lateinit var userRepo: UserRepository + lateinit var userRepo: UserRepository lateinit var spec: RequestSpecification lateinit var fixtures: FixtureFactory diff --git a/realworld-app/web/src/test/kotlin/io/realworld/CommentTests.kt b/realworld-app/web/src/test/kotlin/io/realworld/CommentTests.kt index dc92d1d..b2c9397 100644 --- a/realworld-app/web/src/test/kotlin/io/realworld/CommentTests.kt +++ b/realworld-app/web/src/test/kotlin/io/realworld/CommentTests.kt @@ -50,7 +50,7 @@ class CommentTests { lateinit var auth: Auth @Autowired - lateinit var userRepo: UserRepository + lateinit var userRepo: UserRepository lateinit var spec: RequestSpecification lateinit var fixtures: FixtureFactory diff --git a/realworld-app/web/src/test/kotlin/io/realworld/FavoriteTests.kt b/realworld-app/web/src/test/kotlin/io/realworld/FavoriteTests.kt index 37bbdb5..96c91bd 100644 --- a/realworld-app/web/src/test/kotlin/io/realworld/FavoriteTests.kt +++ b/realworld-app/web/src/test/kotlin/io/realworld/FavoriteTests.kt @@ -41,7 +41,7 @@ class FavoriteTests { lateinit var auth: Auth @Autowired - lateinit var userRepo: UserRepository + lateinit var userRepo: UserRepository lateinit var spec: RequestSpecification lateinit var fixtures: FixtureFactory diff --git a/realworld-app/web/src/test/kotlin/io/realworld/ProfileTests.kt b/realworld-app/web/src/test/kotlin/io/realworld/ProfileTests.kt index 8ecd206..dd97d10 100644 --- a/realworld-app/web/src/test/kotlin/io/realworld/ProfileTests.kt +++ b/realworld-app/web/src/test/kotlin/io/realworld/ProfileTests.kt @@ -1,5 +1,6 @@ package io.realworld +import arrow.effects.ForIO import io.realworld.domain.common.Auth import io.realworld.persistence.UserRepository import io.realworld.persistence.UserTbl @@ -35,7 +36,7 @@ class ProfileTests { lateinit var auth: Auth @Autowired - lateinit var userRepo: UserRepository + lateinit var userRepo: UserRepository lateinit var spec: RequestSpecification lateinit var fixtures: FixtureFactory diff --git a/realworld-app/web/src/test/kotlin/io/realworld/UserTests.kt b/realworld-app/web/src/test/kotlin/io/realworld/UserTests.kt index ba7acd8..a47a49d 100644 --- a/realworld-app/web/src/test/kotlin/io/realworld/UserTests.kt +++ b/realworld-app/web/src/test/kotlin/io/realworld/UserTests.kt @@ -1,5 +1,6 @@ package io.realworld +import arrow.effects.ForIO import io.realworld.domain.common.Auth import io.realworld.persistence.UserRepository import io.realworld.persistence.UserTbl @@ -47,7 +48,7 @@ class UserTests { @Autowired lateinit var auth: Auth - @SpyBean lateinit var userRepo: UserRepository + @SpyBean lateinit var userRepo: UserRepository lateinit var spec: RequestSpecification lateinit var fixtures: FixtureFactory diff --git a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt index 7bb7686..7b21945 100644 --- a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt +++ b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt @@ -110,7 +110,7 @@ private fun Comment.Companion.from(row: CommentRow, deps: CommentDeps) = Comment class ArticleRepository( val jdbcTemplate: NamedParameterJdbcTemplate, - val userRepo: UserRepository, + val userRepo: UserRepository, MD: MonadDefer ) : MonadDefer by MD { diff --git a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt index cd440b5..186ef8a 100644 --- a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt +++ b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt @@ -3,6 +3,7 @@ package io.realworld.persistence import arrow.core.Option import arrow.core.toOption import arrow.effects.IO +import arrow.effects.typeclasses.MonadDefer import io.realworld.domain.users.User import io.realworld.domain.users.UserAndPassword import io.realworld.domain.users.UserId @@ -17,7 +18,10 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate import java.sql.ResultSet import java.util.UUID -open class UserRepository(val jdbcTemplate: NamedParameterJdbcTemplate) { +open class UserRepository( + val jdbcTemplate: NamedParameterJdbcTemplate, + MD: MonadDefer +) : MonadDefer by MD { fun User.Companion.fromRs(rs: ResultSet) = with(UserTbl) { User( From 8925f7b30d17d35b9116829146da26e6e6eee1f0 Mon Sep 17 00:00:00 2001 From: Isto Nikula Date: Sat, 12 Jan 2019 21:01:05 +0200 Subject: [PATCH 03/10] refactor(login-user): remove IO from domain and repo --- .../main/kotlin/io/realworld/users/Routes.kt | 11 +++++++---- .../io/realworld/domain/users/Dependencies.kt | 3 ++- .../io/realworld/domain/users/UseCases.kt | 18 ++++++++++-------- .../io/realworld/persistence/UserRepository.kt | 18 +++++++++--------- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt b/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt index 645f62c..8cbbbde 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt @@ -2,6 +2,9 @@ package io.realworld.users import arrow.core.Option import arrow.effects.ForIO +import arrow.effects.IO +import arrow.effects.fix +import arrow.effects.instances.io.monadDefer.monadDefer import io.realworld.FieldError import io.realworld.UnauthorizedException import io.realworld.domain.common.Auth @@ -84,15 +87,15 @@ class UserController( @PostMapping("/api/users/login") fun login(@Valid @RequestBody login: LoginDto): ResponseEntity { - return object : LoginUserUseCase { + return object : LoginUserUseCase { override val auth = auth0 - override val getUser: GetUserByEmail = repo::findByEmail + override val getUser: GetUserByEmail = repo::findByEmail }.run { LoginUserCommand( email = login.email, password = login.password - ).runUseCase() - }.runWriteTx(txManager).fold( + ).runUseCase(IO.monadDefer()) + }.fix().runWriteTx(txManager).fold( { throw UnauthorizedException() }, { ResponseEntity.ok().body(UserResponse.fromDomain(it)) } ) diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/users/Dependencies.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/users/Dependencies.kt index 30f9d1d..75ed2c5 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/users/Dependencies.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/users/Dependencies.kt @@ -1,5 +1,6 @@ package io.realworld.domain.users +import arrow.Kind import arrow.core.Either import arrow.core.Option import arrow.effects.IO @@ -10,7 +11,7 @@ typealias CreateUser = (user: ValidUserRegistration) -> IO typealias ValidateUserUpdate = (update: UserUpdate, current: User) -> IO> typealias UpdateUser = (update: ValidUserUpdate, current: User) -> IO -typealias GetUserByEmail = (email: String) -> IO> +typealias GetUserByEmail = (email: String) -> Kind> typealias ExistsByEmail = (email: String) -> IO typealias ExistsByUsername = (username: String) -> IO diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt index 64ae8d3..6c211e6 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt @@ -1,5 +1,6 @@ package io.realworld.domain.users +import arrow.Kind import arrow.core.Either import arrow.core.left import arrow.core.right @@ -9,6 +10,7 @@ import arrow.effects.ForIO import arrow.effects.IO import arrow.effects.fix import arrow.effects.instances.io.monad.monad +import arrow.effects.typeclasses.MonadDefer import arrow.instances.monad import arrow.typeclasses.binding import io.realworld.domain.common.Auth @@ -44,26 +46,26 @@ interface RegisterUserUseCase { } } -interface LoginUserUseCase { +interface LoginUserUseCase { val auth: Auth - val getUser: GetUserByEmail + val getUser: GetUserByEmail - fun LoginUserCommand.runUseCase(): IO> { + fun LoginUserCommand.runUseCase(MD: MonadDefer): Kind> { val cmd = this - return EitherT.monad(IO.monad()).binding { + return EitherT.monad(MD).binding { val userAndPassword = EitherT( - getUser(cmd.email).map { - it.toEither { UserLoginError.BadCredentials } + MD.run { + getUser(cmd.email).map { it.toEither { UserLoginError.BadCredentials } } } ).bind() - EitherT(IO.just( + EitherT(MD.just( when (auth.checkPassword(cmd.password, userAndPassword.encryptedPassword)) { true -> userAndPassword.right() false -> UserLoginError.BadCredentials.left() } )).bind() userAndPassword.user - }.value().fix() + }.value() } } diff --git a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt index 186ef8a..73a5dc2 100644 --- a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt +++ b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt @@ -1,5 +1,6 @@ package io.realworld.persistence +import arrow.Kind import arrow.core.Option import arrow.core.toOption import arrow.effects.IO @@ -19,11 +20,11 @@ import java.sql.ResultSet import java.util.UUID open class UserRepository( - val jdbcTemplate: NamedParameterJdbcTemplate, + private val jdbcTemplate: NamedParameterJdbcTemplate, MD: MonadDefer ) : MonadDefer by MD { - fun User.Companion.fromRs(rs: ResultSet) = with(UserTbl) { + private fun User.Companion.fromRs(rs: ResultSet) = with(UserTbl) { User( id = UUID.fromString(rs.getString(id)).userId(), email = rs.getString(email), @@ -34,7 +35,7 @@ open class UserRepository( ) } - fun UserAndPassword.Companion.fromRs(rs: ResultSet) = with(UserTbl) { + private fun UserAndPassword.Companion.fromRs(rs: ResultSet) = with(UserTbl) { UserAndPassword(User.fromRs(rs), rs.getString(password)) } @@ -97,15 +98,14 @@ open class UserRepository( ).toOption() } - fun findByEmail(email: String): IO> = - IO { + fun findByEmail(email: String): Kind> = + defer { DataAccessUtils.singleResult( jdbcTemplate.query( "SELECT * FROM ${UserTbl.table} WHERE ${UserTbl.email.eq()}", - mapOf(UserTbl.email to email), - { rs, _ -> UserAndPassword.fromRs(rs) } - ) - ).toOption() + mapOf(UserTbl.email to email) + ) { rs, _ -> UserAndPassword.fromRs(rs) } + ).toOption().just() } fun findByUsername(username: String): IO> = From 371b51a429b45229c069ea2a0510caf1994e8658 Mon Sep 17 00:00:00 2001 From: Isto Nikula Date: Sun, 13 Jan 2019 04:23:11 +0200 Subject: [PATCH 04/10] refactor(*): remove IO from domain and repo --- .../main/kotlin/io/realworld/Application.kt | 3 +- .../kotlin/io/realworld/articles/Routes.kt | 82 ++++++----- .../kotlin/io/realworld/profiles/Routes.kt | 23 ++-- .../main/kotlin/io/realworld/users/Routes.kt | 28 ++-- .../test/kotlin/io/realworld/ArticleTests.kt | 9 +- .../test/kotlin/io/realworld/CommentTests.kt | 21 +-- .../test/kotlin/io/realworld/FavoriteTests.kt | 13 +- .../test/kotlin/io/realworld/ProfileTests.kt | 39 +++--- .../src/test/kotlin/io/realworld/UserTests.kt | 17 +-- .../realworld/domain/articles/Dependencies.kt | 35 +++-- .../io/realworld/domain/articles/Services.kt | 27 ++-- .../io/realworld/domain/articles/UseCases.kt | 130 +++++++++--------- .../realworld/domain/profiles/Dependencies.kt | 10 +- .../io/realworld/domain/profiles/UseCases.kt | 41 +++--- .../io/realworld/domain/users/Dependencies.kt | 15 +- .../io/realworld/domain/users/Services.kt | 31 +++-- .../io/realworld/domain/users/UseCases.kt | 36 ++--- .../io/realworld/domain/users/UsersTest.kt | 23 ++-- .../persistence/ArticleRepository.kt | 79 +++++------ .../io/realworld/persistence/QueryUtils.kt | 12 +- .../realworld/persistence/UserRepository.kt | 70 +++++----- 21 files changed, 384 insertions(+), 360 deletions(-) diff --git a/realworld-app/web/src/main/kotlin/io/realworld/Application.kt b/realworld-app/web/src/main/kotlin/io/realworld/Application.kt index dd66e6e..7c0478c 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/Application.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/Application.kt @@ -3,6 +3,7 @@ package io.realworld import arrow.core.getOrElse import arrow.effects.ForIO import arrow.effects.IO +import arrow.effects.fix import arrow.effects.instances.io.monadDefer.monadDefer import io.realworld.domain.common.Auth import io.realworld.domain.common.Settings @@ -41,7 +42,7 @@ class Application : WebMvcConfigurer { lateinit var repo: UserRepository override fun invoke(token: Token): User { - return repo.findById(token.id).unsafeRunSync().map { it.user }.getOrElse { throw UnauthorizedException() } + return repo.findById(token.id).fix().unsafeRunSync().map { it.user }.getOrElse { throw UnauthorizedException() } } } diff --git a/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt b/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt index 5487792..f41e86f 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt @@ -106,19 +106,21 @@ class ArticleController( ) { @PostMapping("/api/articles") fun create(@Valid @RequestBody dto: CreationDto, user: User): ResponseEntity { - val createUniqueSlugSrv = object : CreateUniqueSlugService { + val ioMonadDefer = IO.monadDefer() + val createUniqueSlugSrv = object : CreateUniqueSlugService { override val existsBySlug = articleRepo::existsBySlug + override val MD = ioMonadDefer } - return object : CreateArticleUseCase { + return object : CreateArticleUseCase { override val createUniqueSlug = createUniqueSlugSrv::slufigy override val createArticle = articleRepo::create }.run { CreateArticleCommand( data = dto.toDomain(), user = user - ).runUseCase() - }.runWriteTx(txManager).let { + ).runUseCase(ioMonadDefer) + }.fix().runWriteTx(txManager).let { ResponseEntity.status(HttpStatus.CREATED).body(ArticleResponse.fromDomain(it)) } } @@ -132,15 +134,15 @@ class ArticleController( val user = JwtTokenResolver(auth::parse)( webRequest.authHeader() ).toOption().flatMap { - userRepo.findById(it.id).unsafeRunSync().map { it.user } + userRepo.findById(it.id).fix().unsafeRunSync().map { it.user } } - return object : GetArticlesUseCase { + return object : GetArticlesUseCase { override val getArticles = articleRepo::getArticles override val getArticlesCount = articleRepo::getArticlesCount }.run { - GetArticlesCommand(filter, user).runUseCase() - }.runReadTx(txManager).let { + GetArticlesCommand(filter, user).runUseCase(IO.monadDefer()) + }.fix().runReadTx(txManager).let { ResponseEntity.ok(ArticlesResponse.fromDomain(it)) } } @@ -169,14 +171,14 @@ class ArticleController( val user = JwtTokenResolver(auth::parse)( webRequest.authHeader() ).toOption().flatMap { - userRepo.findById(it.id).unsafeRunSync().map { it.user } + userRepo.findById(it.id).fix().unsafeRunSync().map { it.user } } - return object : GetArticleUseCase { + return object : GetArticleUseCase { override val getArticleBySlug = articleRepo::getBySlug }.run { GetArticleCommand(slug, user).runUseCase() - }.runReadTx(txManager).fold( + }.fix().runReadTx(txManager).fold( { ResponseEntity.notFound().build() }, { ResponseEntity.ok(ArticleResponse.fromDomain(it)) } ) @@ -187,12 +189,12 @@ class ArticleController( @PathVariable("slug") slug: String, user: User ): ResponseEntity { - return object : DeleteArticleUseCase { + return object : DeleteArticleUseCase { override val getArticleBySlug = articleRepo::getBySlug override val deleteArticle = articleRepo::deleteArticle }.run { - DeleteArticleCommand(slug, user).runUseCase() - }.runWriteTx(txManager).fold( + DeleteArticleCommand(slug, user).runUseCase(IO.monadDefer()) + }.fix().runWriteTx(txManager).fold( { when (it) { is ArticleDeleteError.NotAuthor -> throw ForbiddenException() @@ -209,21 +211,25 @@ class ArticleController( @Valid @RequestBody update: UpdateDto, user: User ): ResponseEntity { - val createUniqueSlugSrv = object : CreateUniqueSlugService { + val ioMonadDefer = IO.monadDefer() + val createUniqueSlugSrv = object : CreateUniqueSlugService { override val existsBySlug = articleRepo::existsBySlug + override val MD = ioMonadDefer } - val validateUpdateSrv = object : ValidateArticleUpdateService { + val validateUpdateSrv = object : ValidateArticleUpdateService { override val createUniqueSlug = createUniqueSlugSrv::slufigy override val getArticleBySlug = articleRepo::getBySlug + override val MD = ioMonadDefer } - return object : UpdateArticleUseCase { - override val validateUpdate: ValidateArticleUpdate = { x, y, z -> validateUpdateSrv.run { x.validate(y, z) } } + return object : UpdateArticleUseCase { + override val validateUpdate: ValidateArticleUpdate = + { x, y, z -> validateUpdateSrv.run { x.validate(y, z) } } override val updateArticle = articleRepo::updateArticle }.run { - UpdateArticleCommand(update.toDomain(), slug, user).runUseCase() - }.runWriteTx(txManager).fold( + UpdateArticleCommand(update.toDomain(), slug, user).runUseCase(ioMonadDefer) + }.fix().runWriteTx(txManager).fold( { when (it) { is ArticleUpdateError.NotAuthor -> throw ForbiddenException() @@ -240,12 +246,12 @@ class ArticleController( user: User ): ResponseEntity { - return object : FavoriteUseCase { + return object : FavoriteUseCase { override val getArticleBySlug = articleRepo::getBySlug override val addFavorite = articleRepo::addFavorite }.run { - FavoriteArticleCommand(slug, user).runUseCase() - }.runWriteTx(txManager).fold( + FavoriteArticleCommand(slug, user).runUseCase(IO.monadDefer()) + }.fix().runWriteTx(txManager).fold( { when (it) { is ArticleFavoriteError.Author -> throw ForbiddenException() @@ -261,12 +267,12 @@ class ArticleController( @PathVariable("slug") slug: String, user: User ): ResponseEntity { - return object : UnfavoriteUseCase { + return object : UnfavoriteUseCase { override val getArticleBySlug = articleRepo::getBySlug override val removeFavorite = articleRepo::removeFavorite }.run { - UnfavoriteArticleCommand(slug, user).runUseCase() - }.runWriteTx(txManager).fold( + UnfavoriteArticleCommand(slug, user).runUseCase(IO.monadDefer()) + }.fix().runWriteTx(txManager).fold( { when (it) { is ArticleUnfavoriteError.NotFound -> ResponseEntity.notFound().build() @@ -285,15 +291,15 @@ class ArticleController( val user = JwtTokenResolver(auth::parse)( webRequest.authHeader() ).toOption().flatMap { - userRepo.findById(it.id).unsafeRunSync().map { it.user } + userRepo.findById(it.id).fix().unsafeRunSync().map { it.user } } - return object : GetCommentsUseCase { + return object : GetCommentsUseCase { override val getArticleBySlug = articleRepo::getBySlug override val getComments = articleRepo::getComments }.run { - GetCommentsCommand(slug, user).runUseCase() - }.runReadTx(txManager).fold( + GetCommentsCommand(slug, user).runUseCase(IO.monadDefer()) + }.fix().runReadTx(txManager).fold( { ResponseEntity.notFound().build() }, { ResponseEntity.ok(CommentsResponse.fromDomain(it)) } ) @@ -305,12 +311,12 @@ class ArticleController( @Valid @RequestBody comment: CommentDto, user: User ): ResponseEntity { - return object : CommentUseCase { + return object : CommentUseCase { override val getArticleBySlug = articleRepo::getBySlug override val addComment = articleRepo::addComment }.run { - CommentArticleCommand(slug, comment.body, user).runUsecase() - }.runWriteTx(txManager).fold( + CommentArticleCommand(slug, comment.body, user).runUsecase(IO.monadDefer()) + }.fix().runWriteTx(txManager).fold( { when (it) { is ArticleCommentError.NotFound -> ResponseEntity.notFound().build() @@ -326,13 +332,13 @@ class ArticleController( @PathVariable("id") commentId: Long, user: User ): ResponseEntity { - return object : DeleteCommentUseCase { + return object : DeleteCommentUseCase { override val getArticleBySlug = articleRepo::getBySlug override val deleteComment = articleRepo::deleteComment override val getComment = articleRepo::getComment }.run { - DeleteCommentCommand(slug, commentId, user).runUseCase() - }.runWriteTx(txManager).fold( + DeleteCommentCommand(slug, commentId, user).runUseCase(IO.monadDefer()) + }.fix().runWriteTx(txManager).fold( { when (it) { is ArticleCommentDeleteError.ArticleNotFound -> ResponseEntity.notFound().build() @@ -346,11 +352,11 @@ class ArticleController( @GetMapping("/api/tags") fun getTags(): ResponseEntity { - return object : GetTagsUseCase { + return object : GetTagsUseCase { override val getTags = articleRepo::getTags }.run { GetTagsCommand.runUseCase() - }.runReadTx(txManager).let { + }.fix().runReadTx(txManager).let { ResponseEntity.ok(TagsResponse(it)) } } diff --git a/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt b/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt index 2606cd3..c102e88 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt @@ -1,6 +1,9 @@ package io.realworld.profiles import arrow.effects.ForIO +import arrow.effects.IO +import arrow.effects.fix +import arrow.effects.instances.io.monadDefer.monadDefer import io.realworld.JwtTokenResolver import io.realworld.authHeader import io.realworld.domain.common.Auth @@ -46,15 +49,15 @@ class ProfileController( val user = JwtTokenResolver(auth::parse)( webRequest.authHeader() ).toOption().flatMap { - repo.findById(it.id).unsafeRunSync().map { it.user } + repo.findById(it.id).fix().unsafeRunSync().map { it.user } } - return object : GetProfileUseCase { + return object : GetProfileUseCase { override val getUser = repo::findByUsername override val hasFollower = repo::hasFollower }.run { - GetProfileCommand(username, user).runUseCase() - }.runReadTx(txManager).fold( + GetProfileCommand(username, user).runUseCase(IO.monadDefer()) + }.fix().runReadTx(txManager).fold( { ResponseEntity.notFound().build() }, { ResponseEntity.ok(ProfileResponse.fromDomain(it)) } ) @@ -65,12 +68,12 @@ class ProfileController( @PathVariable("username") username: String, current: User ): ResponseEntity { - return object : FollowUseCase { + return object : FollowUseCase { override val addFollower = repo::addFollower override val getUser = repo::findByUsername }.run { - FollowCommand(username, current).runUseCase() - }.runWriteTx(txManager).fold( + FollowCommand(username, current).runUseCase(IO.monadDefer()) + }.fix().runWriteTx(txManager).fold( { ResponseEntity.notFound().build() }, { ResponseEntity.ok(ProfileResponse.fromDomain(it)) } ) @@ -81,12 +84,12 @@ class ProfileController( @PathVariable("username") username: String, current: User ): ResponseEntity { - return object : UnfollowUseCase { + return object : UnfollowUseCase { override val getUser = repo::findByUsername override val removeFollower = repo::removeFollower }.run { - UnfollowCommand(username, current).runUseCase() - }.runWriteTx(txManager).fold( + UnfollowCommand(username, current).runUseCase(IO.monadDefer()) + }.fix().runWriteTx(txManager).fold( { ResponseEntity.notFound().build() }, { ResponseEntity.ok(ProfileResponse.fromDomain(it)) } ) diff --git a/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt b/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt index 8cbbbde..7747d89 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt @@ -56,23 +56,25 @@ class UserController( @PostMapping("/api/users") fun register(@Valid @RequestBody registration: RegistrationDto): ResponseEntity { - val validateUserSrv = object : ValidateUserService { + val ioMonadDefer = IO.monadDefer() + val validateUserSrv = object : ValidateUserService { override val auth = auth0 override val existsByEmail = repo::existsByEmail override val existsByUsername = repo::existsByUsername + override val MD = ioMonadDefer } - return object : RegisterUserUseCase { + return object : RegisterUserUseCase { override val auth = auth0 - override val createUser: CreateUser = repo::create - override val validateUser: ValidateUserRegistration = { x -> validateUserSrv.run { x.validate() } } + override val createUser: CreateUser = repo::create + override val validateUser: ValidateUserRegistration = { x -> validateUserSrv.run { x.validate() } } }.run { RegisterUserCommand(UserRegistration( username = registration.username, email = registration.email, password = registration.password - )).runUseCase() - }.runWriteTx(txManager).fold( + )).runUseCase(ioMonadDefer) + }.fix().runWriteTx(txManager).fold( { when (it) { is UserRegistrationError.EmailAlreadyTaken -> @@ -103,16 +105,18 @@ class UserController( @PutMapping("/api/user") fun update(@Valid @RequestBody update: UserUpdateDto, user: User): ResponseEntity { - val validateUpdateSrv = object : ValidateUserUpdateService { + val ioMonadDefer = IO.monadDefer() + val validateUpdateSrv = object : ValidateUserUpdateService { override val auth = auth0 override val existsByEmail = repo::existsByEmail override val existsByUsername = repo::existsByUsername + override val MD = ioMonadDefer } - return object : UpdateUserUseCase { + return object : UpdateUserUseCase { override val auth = auth0 - override val validateUpdate: ValidateUserUpdate = { x, y -> validateUpdateSrv.run { x.validate(y) } } - override val updateUser: UpdateUser = repo::update + override val validateUpdate: ValidateUserUpdate = { x, y -> validateUpdateSrv.run { x.validate(y) } } + override val updateUser: UpdateUser = repo::update }.run { UpdateUserCommand( data = UserUpdate( @@ -123,8 +127,8 @@ class UserController( image = Option.fromNullable(update.image) ), current = user - ).runUseCase() - }.runWriteTx(txManager).fold( + ).runUseCase(ioMonadDefer) + }.fix().runWriteTx(txManager).fold( { when (it) { is UserUpdateError.EmailAlreadyTaken -> diff --git a/realworld-app/web/src/test/kotlin/io/realworld/ArticleTests.kt b/realworld-app/web/src/test/kotlin/io/realworld/ArticleTests.kt index 4f8dfb6..1e21318 100644 --- a/realworld-app/web/src/test/kotlin/io/realworld/ArticleTests.kt +++ b/realworld-app/web/src/test/kotlin/io/realworld/ArticleTests.kt @@ -1,6 +1,7 @@ package io.realworld import arrow.effects.ForIO +import arrow.effects.fix import io.realworld.articles.ArticleResponse import io.realworld.articles.ArticleResponseDto import io.realworld.articles.ArticlesResponse @@ -216,16 +217,14 @@ class ArticleTests { .then() .statusCode(201) .toDto().apply { - assertThat(article).isEqualToIgnoringGivenFields(expected, "createdAt", "updatedAt") - assertThat(article.createdAt).isNotNull() - assertThat(article.createdAt).isEqualTo(article.updatedAt) + article.assert(expected) } client.get("/api/articles/${expected.title.slugify()}") .then() .statusCode(200) .toDto().apply { - assertThat(article).isEqualToIgnoringGivenFields(expected, "createdAt", "updatedAt") + article.assert(expected) } } @@ -949,7 +948,7 @@ class ArticleTests { } private fun createUser(user: TestUser) = - userRepo.create(fixtures.validTestUserRegistration(user.username, user.email)).unsafeRunSync() + userRepo.create(fixtures.validTestUserRegistration(user.username, user.email)).fix().unsafeRunSync() private fun UserClient.Companion.from(user: TestUser) = createUser(user).let { UserClient(it, ApiClient(spec, it.token)) } diff --git a/realworld-app/web/src/test/kotlin/io/realworld/CommentTests.kt b/realworld-app/web/src/test/kotlin/io/realworld/CommentTests.kt index b2c9397..8382e36 100644 --- a/realworld-app/web/src/test/kotlin/io/realworld/CommentTests.kt +++ b/realworld-app/web/src/test/kotlin/io/realworld/CommentTests.kt @@ -1,6 +1,7 @@ package io.realworld import arrow.effects.ForIO +import arrow.effects.fix import io.realworld.articles.CommentDto import io.realworld.articles.CommentResponse import io.realworld.domain.common.Auth @@ -78,7 +79,7 @@ class CommentTests { val cheeta = createUser("cheeta") val jane = createUser("jane") - val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).unsafeRunSync() + val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).fix().unsafeRunSync() val cheetaClient = ApiClient(spec, cheeta.token) cheetaClient.post("/api/articles/${janesArticle.slug}/comments", TestComments.Jacobian.req) @@ -99,7 +100,7 @@ class CommentTests { fun `comment article, author`() { val jane = createUser("jane") - val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).unsafeRunSync() + val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).fix().unsafeRunSync() val janeClient = ApiClient(spec, jane.token) janeClient.post("/api/articles/${janesArticle.slug}/comments", TestComments.Jacobian.req) @@ -112,7 +113,7 @@ class CommentTests { val cheeta = createUser("cheeta") val jane = createUser("jane") - val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).unsafeRunSync() + val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).fix().unsafeRunSync() val cheetaClient = ApiClient(spec, cheeta.token) @@ -129,7 +130,7 @@ class CommentTests { @Test fun `comment article requires auth`() { val jane = createUser("jane") - val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).unsafeRunSync() + val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).fix().unsafeRunSync() ApiClient(spec).post("/api/articles/${janesArticle.slug}/comments", TestComments.Jacobian.req) .then() @@ -141,7 +142,7 @@ class CommentTests { val cheeta = createUser("cheeta") val jane = createUser("jane") - val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).unsafeRunSync() + val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).fix().unsafeRunSync() val cheetaClient = ApiClient(spec, cheeta.token) @@ -161,7 +162,7 @@ class CommentTests { val cheeta = createUser("cheeta") val jane = createUser("jane") - val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).unsafeRunSync() + val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).fix().unsafeRunSync() val cheetaClient = ApiClient(spec, cheeta.token) @@ -181,7 +182,7 @@ class CommentTests { val cheeta = createUser("cheeta") val jane = createUser("jane") - val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).unsafeRunSync() + val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).fix().unsafeRunSync() val cheetaClient = ApiClient(spec, cheeta.token) cheetaClient.delete("/api/articles/${janesArticle.slug}/comments/${Long.MAX_VALUE}") @@ -194,7 +195,7 @@ class CommentTests { val cheeta = createUser("cheeta") val jane = createUser("jane") - val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).unsafeRunSync() + val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).fix().unsafeRunSync() val janeClient = ApiClient(spec, jane.token) val cheetaClient = ApiClient(spec, cheeta.token) @@ -218,7 +219,7 @@ class CommentTests { val jane = createUser("jane") val tarzan = createUser("tarzan") - val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).unsafeRunSync() + val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).fix().unsafeRunSync() val tarzanClient = ApiClient(spec, tarzan.token) tarzanClient.get("/api/articles/${janesArticle.slug}/comments") @@ -259,5 +260,5 @@ class CommentTests { } private fun createUser(username: String) = - userRepo.create(fixtures.validTestUserRegistration(username, "$username@realworld.io")).unsafeRunSync() + userRepo.create(fixtures.validTestUserRegistration(username, "$username@realworld.io")).fix().unsafeRunSync() } diff --git a/realworld-app/web/src/test/kotlin/io/realworld/FavoriteTests.kt b/realworld-app/web/src/test/kotlin/io/realworld/FavoriteTests.kt index 96c91bd..fce9a90 100644 --- a/realworld-app/web/src/test/kotlin/io/realworld/FavoriteTests.kt +++ b/realworld-app/web/src/test/kotlin/io/realworld/FavoriteTests.kt @@ -1,6 +1,7 @@ package io.realworld import arrow.effects.ForIO +import arrow.effects.fix import io.realworld.articles.UpdateDto import io.realworld.domain.common.Auth import io.realworld.persistence.ArticleRepository @@ -70,7 +71,7 @@ class FavoriteTests { val jane = createUser("jane") val tarzan = createUser("tarzan") - val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).unsafeRunSync() + val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).fix().unsafeRunSync() val tarzanClient = ApiClient(spec, tarzan.token) tarzanClient.get("/api/articles/${janesArticle.slug}") @@ -117,7 +118,7 @@ class FavoriteTests { @Test fun `favorite article, author`() { val jane = createUser("jane") - val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).unsafeRunSync() + val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).fix().unsafeRunSync() val janeClient = ApiClient(spec, jane.token) janeClient.get("/api/articles/${janesArticle.slug}") @@ -138,7 +139,7 @@ class FavoriteTests { @Test fun `favoriting already favorited acticle has no effect`() { val jane = createUser("jane") - val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).unsafeRunSync() + val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).fix().unsafeRunSync() val tarzan = createUser("tarzan") val tarzanClient = ApiClient(spec, tarzan.token) @@ -170,7 +171,7 @@ class FavoriteTests { val jane = createUser("jane") val tarzan = createUser("tarzan") - val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).unsafeRunSync() + val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).fix().unsafeRunSync() val tarzanClient = ApiClient(spec, tarzan.token) tarzanClient.post("/api/articles/${janesArticle.slug}/favorite") @@ -214,7 +215,7 @@ class FavoriteTests { val janeClient = ApiClient(spec, jane.token) val tarzanClient = ApiClient(spec, tarzan.token) - val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).unsafeRunSync() + val janesArticle = articleRepo.create(fixtures.validTestArticleCreation(), jane).fix().unsafeRunSync() val updateReq = UpdateRequest(UpdateDto(description = "updated.${janesArticle.description}")) janeClient.put("/api/articles/${janesArticle.slug}", updateReq) @@ -258,5 +259,5 @@ class FavoriteTests { } private fun createUser(username: String) = - userRepo.create(fixtures.validTestUserRegistration(username, "$username@realworld.io")).unsafeRunSync() + userRepo.create(fixtures.validTestUserRegistration(username, "$username@realworld.io")).fix().unsafeRunSync() } diff --git a/realworld-app/web/src/test/kotlin/io/realworld/ProfileTests.kt b/realworld-app/web/src/test/kotlin/io/realworld/ProfileTests.kt index dd97d10..612bb80 100644 --- a/realworld-app/web/src/test/kotlin/io/realworld/ProfileTests.kt +++ b/realworld-app/web/src/test/kotlin/io/realworld/ProfileTests.kt @@ -1,6 +1,7 @@ package io.realworld import arrow.effects.ForIO +import arrow.effects.fix import io.realworld.domain.common.Auth import io.realworld.persistence.UserRepository import io.realworld.persistence.UserTbl @@ -64,12 +65,12 @@ class ProfileTests { val user1 = fixtures.validTestUserRegistration("foo", "foo@realworld.io") val user2 = fixtures.validTestUserRegistration("bar", "bar@realworld.io") val user3 = fixtures.validTestUserRegistration("baz", "baz@realworld.io") - userRepo.create(user1).unsafeRunSync() - userRepo.create(user2).unsafeRunSync() - userRepo.create(user3).unsafeRunSync() + userRepo.create(user1).fix().unsafeRunSync() + userRepo.create(user2).fix().unsafeRunSync() + userRepo.create(user3).fix().unsafeRunSync() - userRepo.addFollower(user2.id, user1.id).unsafeRunSync() - userRepo.addFollower(user3.id, user1.id).unsafeRunSync() + userRepo.addFollower(user2.id, user1.id).fix().unsafeRunSync() + userRepo.addFollower(user3.id, user1.id).fix().unsafeRunSync() val user1Client = ApiClient(spec, user1.token) val user2Client = ApiClient(spec, user2.token) @@ -102,10 +103,10 @@ class ProfileTests { fun `get profile without token`() { val user1 = fixtures.validTestUserRegistration("foo", "foo@realworld.io") val user2 = fixtures.validTestUserRegistration("bar", "bar@realworld.io") - userRepo.create(user1).unsafeRunSync() - userRepo.create(user2).unsafeRunSync() + userRepo.create(user1).fix().unsafeRunSync() + userRepo.create(user2).fix().unsafeRunSync() - userRepo.addFollower(user1.id, user2.id).unsafeRunSync() + userRepo.addFollower(user1.id, user2.id).fix().unsafeRunSync() ApiClient(spec).get("/api/profiles/bar") .then() @@ -118,8 +119,8 @@ class ProfileTests { fun `follow`() { val user1 = fixtures.validTestUserRegistration("foo", "foo@realworld.io") val user2 = fixtures.validTestUserRegistration("bar", "bar@realworld.io") - userRepo.create(user1).unsafeRunSync() - userRepo.create(user2).unsafeRunSync() + userRepo.create(user1).fix().unsafeRunSync() + userRepo.create(user2).fix().unsafeRunSync() val client = ApiClient(spec, user1.token) @@ -139,7 +140,7 @@ class ProfileTests { @Test fun `follow phantom`() { val user1 = fixtures.validTestUserRegistration("foo", "foo@realworld.io") - userRepo.create(user1).unsafeRunSync() + userRepo.create(user1).fix().unsafeRunSync() ApiClient(spec, user1.token).post("/api/profiles/bar/follow") .then() @@ -150,9 +151,9 @@ class ProfileTests { fun `follow already followed`() { val user1 = fixtures.validTestUserRegistration("foo", "foo@realworld.io") val user2 = fixtures.validTestUserRegistration("bar", "bar@realworld.io") - userRepo.create(user1).unsafeRunSync() - userRepo.create(user2).unsafeRunSync() - userRepo.addFollower(user2.id, user1.id).unsafeRunSync() + userRepo.create(user1).fix().unsafeRunSync() + userRepo.create(user2).fix().unsafeRunSync() + userRepo.addFollower(user2.id, user1.id).fix().unsafeRunSync() ApiClient(spec, user1.token).post("/api/profiles/bar/follow") .then() @@ -163,8 +164,8 @@ class ProfileTests { fun `unfollow`() { val user1 = fixtures.validTestUserRegistration("foo", "foo@realworld.io") val user2 = fixtures.validTestUserRegistration("bar", "bar@realworld.io") - userRepo.create(user1).unsafeRunSync() - userRepo.create(user2).unsafeRunSync() + userRepo.create(user1).fix().unsafeRunSync() + userRepo.create(user2).fix().unsafeRunSync() val client = ApiClient(spec, user1.token) @@ -184,7 +185,7 @@ class ProfileTests { @Test fun `unfollow phantom`() { val user1 = fixtures.validTestUserRegistration("foo", "foo@realworld.io") - userRepo.create(user1).unsafeRunSync() + userRepo.create(user1).fix().unsafeRunSync() ApiClient(spec, user1.token).delete("/api/profiles/bar/follow") .then() @@ -195,8 +196,8 @@ class ProfileTests { fun `unfollow not followed`() { val user1 = fixtures.validTestUserRegistration("foo", "foo@realworld.io") val user2 = fixtures.validTestUserRegistration("bar", "bar@realworld.io") - userRepo.create(user1).unsafeRunSync() - userRepo.create(user2).unsafeRunSync() + userRepo.create(user1).fix().unsafeRunSync() + userRepo.create(user2).fix().unsafeRunSync() ApiClient(spec, user1.token).delete("/api/profiles/bar/follow") .then() diff --git a/realworld-app/web/src/test/kotlin/io/realworld/UserTests.kt b/realworld-app/web/src/test/kotlin/io/realworld/UserTests.kt index a47a49d..899a503 100644 --- a/realworld-app/web/src/test/kotlin/io/realworld/UserTests.kt +++ b/realworld-app/web/src/test/kotlin/io/realworld/UserTests.kt @@ -1,6 +1,7 @@ package io.realworld import arrow.effects.ForIO +import arrow.effects.fix import io.realworld.domain.common.Auth import io.realworld.persistence.UserRepository import io.realworld.persistence.UserTbl @@ -103,7 +104,7 @@ class UserTests { @Test fun `cannot register already existing username`() { - userRepo.create(fixtures.validTestUserRegistration(TestUser.username, TestUser.email)).unsafeRunSync() + userRepo.create(fixtures.validTestUserRegistration(TestUser.username, TestUser.email)).fix().unsafeRunSync() val regReq = RegistrationRequest(RegistrationDto( username = TestUser.username, email = "unique.${TestUser.email}", @@ -117,7 +118,7 @@ class UserTests { @Test fun `cannot register already existing email`() { - userRepo.create(fixtures.validTestUserRegistration(TestUser.username, TestUser.email)).unsafeRunSync() + userRepo.create(fixtures.validTestUserRegistration(TestUser.username, TestUser.email)).fix().unsafeRunSync() val regReq = RegistrationRequest(RegistrationDto( username = "unique", email = TestUser.email, @@ -158,7 +159,7 @@ class UserTests { @Test fun `current user is resolved from token`() { val registered = fixtures.validTestUserRegistration(TestUser.username, TestUser.email) - userRepo.create(registered).unsafeRunSync() + userRepo.create(registered).fix().unsafeRunSync() val actual: UserResponse = ApiClient(spec).get("/api/user", registered.token) .then() @@ -180,7 +181,7 @@ class UserTests { @Test fun `invalid password is reported as 401`() { val registered = fixtures.validTestUserRegistration(TestUser.username, TestUser.email) - userRepo.create(registered).unsafeRunSync() + userRepo.create(registered).fix().unsafeRunSync() with(LoginRequest(LoginDto(email = registered.email, password = "invalid"))) { ApiClient(spec).post("/api/users/login", this).then().statusCode(401) @@ -190,7 +191,7 @@ class UserTests { @Test fun `update user email`() { val registered = fixtures.validTestUserRegistration(TestUser.username, TestUser.email) - userRepo.create(registered).unsafeRunSync() + userRepo.create(registered).fix().unsafeRunSync() UserUpdateRequest(UserUpdateDto(email = "updated.${registered.email}")).apply { val actual: UserResponse = ApiClient(spec).put("/api/user", this, registered.token) @@ -204,7 +205,7 @@ class UserTests { @Test fun `update user password`() { val registered = fixtures.validTestUserRegistration(TestUser.username, TestUser.email) - userRepo.create(registered).unsafeRunSync() + userRepo.create(registered).fix().unsafeRunSync() val client = ApiClient(spec) @@ -219,7 +220,7 @@ class UserTests { @Test fun `update user username`() { val registered = fixtures.validTestUserRegistration(TestUser.username, TestUser.email) - userRepo.create(registered).unsafeRunSync() + userRepo.create(registered).fix().unsafeRunSync() UserUpdateRequest(UserUpdateDto(username = "updated.${registered.username}")).apply { val actual: UserResponse = ApiClient(spec).put("/api/user", this, registered.token) @@ -233,7 +234,7 @@ class UserTests { @Test fun `update user username, image, bio`() { val registered = fixtures.validTestUserRegistration(TestUser.username, TestUser.email) - userRepo.create(registered).unsafeRunSync() + userRepo.create(registered).fix().unsafeRunSync() UserUpdateRequest(UserUpdateDto( username = "updated.${registered.username}", diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/articles/Dependencies.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/articles/Dependencies.kt index dbb8a72..ab7c92c 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/articles/Dependencies.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/articles/Dependencies.kt @@ -3,32 +3,31 @@ package io.realworld.domain.articles import arrow.Kind import arrow.core.Either import arrow.core.Option -import arrow.effects.IO import io.realworld.domain.users.User -typealias CreateArticle = (ValidArticleCreation, User) -> IO
+typealias CreateArticle = (ValidArticleCreation, User) -> Kind -typealias ValidateArticleUpdate = (ArticleUpdate, slug: String, User) -> - IO> -typealias UpdateArticle = (ValidArticleUpdate, User) -> IO
+typealias ValidateArticleUpdate = (ArticleUpdate, slug: String, User) -> + Kind> +typealias UpdateArticle = (ValidArticleUpdate, User) -> Kind -typealias CreateUniqueSlug = (title: String) -> IO -typealias ExistsBySlug = (slug: String) -> IO +typealias CreateUniqueSlug = (title: String) -> Kind +typealias ExistsBySlug = (slug: String) -> Kind -typealias GetArticleBySlug = (slug: String, Option) -> IO> -typealias GetArticles = (ArticleFilter, Option) -> IO> -typealias GetArticlesCount = (ArticleFilter) -> IO +typealias GetArticleBySlug = (slug: String, Option) -> Kind> +typealias GetArticles = (ArticleFilter, Option) -> Kind> +typealias GetArticlesCount = (ArticleFilter) -> Kind typealias GetFeeds = (FeedFilter, User) -> Kind> typealias GetFeedsCount = (user: User) -> Kind -typealias DeleteArticle = (ArticleId) -> IO +typealias DeleteArticle = (ArticleId) -> Kind -typealias AddFavorite = (ArticleId, User) -> IO -typealias RemoveFavorite = (ArticleId, User) -> IO +typealias AddFavorite = (ArticleId, User) -> Kind +typealias RemoveFavorite = (ArticleId, User) -> Kind -typealias AddComment = (ArticleId, comment: String, User) -> IO -typealias DeleteComment = (id: Long) -> IO -typealias GetComment = (id: Long, User) -> IO> -typealias GetComments = (ArticleId, Option) -> IO> +typealias AddComment = (ArticleId, comment: String, User) -> Kind +typealias DeleteComment = (id: Long) -> Kind +typealias GetComment = (id: Long, User) -> Kind> +typealias GetComments = (ArticleId, Option) -> Kind> -typealias GetTags = () -> IO> +typealias GetTags = () -> Kind> diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/articles/Services.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/articles/Services.kt index 3e6dfd1..b1493b0 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/articles/Services.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/articles/Services.kt @@ -1,13 +1,12 @@ package io.realworld.domain.articles +import arrow.Kind import arrow.core.Either import arrow.core.getOrElse import arrow.core.left import arrow.core.right import arrow.core.some -import arrow.effects.IO -import arrow.effects.fix -import arrow.effects.instances.io.monad.monad +import arrow.effects.typeclasses.MonadDefer import arrow.typeclasses.binding import com.github.slugify.Slugify import io.realworld.domain.users.User @@ -16,26 +15,28 @@ import java.util.UUID private val slugifier = Slugify() fun String.slugify() = slugifier.slugify(this) -interface CreateUniqueSlugService { - val existsBySlug: ExistsBySlug +interface CreateUniqueSlugService { + val existsBySlug: ExistsBySlug + val MD: MonadDefer - fun slufigy(s: String): IO = IO.monad().binding { + fun slufigy(s: String): Kind = MD.binding { val slugified = s.slugify() var slugCandidate = slugified while (existsBySlug(slugCandidate).bind()) { slugCandidate = "$slugified-${UUID.randomUUID().toString().substring(0, 8)}" } slugCandidate - }.fix() + } } -interface ValidateArticleUpdateService { - val createUniqueSlug: CreateUniqueSlug - val getArticleBySlug: GetArticleBySlug +interface ValidateArticleUpdateService { + val createUniqueSlug: CreateUniqueSlug + val getArticleBySlug: GetArticleBySlug + val MD: MonadDefer - fun ArticleUpdate.validate(slug: String, user: User): IO> { + fun ArticleUpdate.validate(slug: String, user: User): Kind> { val cmd = this - return IO.monad().binding { + return MD.binding { getArticleBySlug(slug, user.some()).bind().fold( { ArticleUpdateError.NotFound.left() }, { @@ -53,6 +54,6 @@ interface ValidateArticleUpdateService { } } ) - }.fix() + } } } diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt index 0b11e19..354894f 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt @@ -10,10 +10,6 @@ import arrow.core.right import arrow.core.some import arrow.data.EitherT import arrow.data.value -import arrow.effects.ForIO -import arrow.effects.IO -import arrow.effects.fix -import arrow.effects.instances.io.monad.monad import arrow.effects.typeclasses.MonadDefer import arrow.instances.monad import arrow.typeclasses.binding @@ -62,13 +58,13 @@ sealed class ArticleCommentDeleteError { object NotAuthor : ArticleCommentDeleteError() } -interface CreateArticleUseCase { - val createUniqueSlug: CreateUniqueSlug - val createArticle: CreateArticle +interface CreateArticleUseCase { + val createUniqueSlug: CreateUniqueSlug + val createArticle: CreateArticle - fun CreateArticleCommand.runUseCase(): IO
{ + fun CreateArticleCommand.runUseCase(MD: MonadDefer): Kind { val cmd = this - return IO.monad().binding { + return MD.binding { val slug = createUniqueSlug(cmd.data.title).bind() createArticle( ValidArticleCreation( @@ -81,24 +77,24 @@ interface CreateArticleUseCase { ), cmd.user ).bind() - }.fix() + } } } -interface GetArticleUseCase { - val getArticleBySlug: GetArticleBySlug +interface GetArticleUseCase { + val getArticleBySlug: GetArticleBySlug - fun GetArticleCommand.runUseCase(): IO> = + fun GetArticleCommand.runUseCase(): Kind> = getArticleBySlug(slug, user) } -interface DeleteArticleUseCase { - val getArticleBySlug: GetArticleBySlug - val deleteArticle: DeleteArticle +interface DeleteArticleUseCase { + val getArticleBySlug: GetArticleBySlug + val deleteArticle: DeleteArticle - fun DeleteArticleCommand.runUseCase(): IO> { + fun DeleteArticleCommand.runUseCase(MD: MonadDefer): Kind> { val cmd = this - return IO.monad().binding { + return MD.binding { getArticleBySlug(cmd.slug, user.some()).bind().fold( { ArticleDeleteError.NotFound.left() }, { @@ -106,17 +102,17 @@ interface DeleteArticleUseCase { else deleteArticle(it.id).bind().right() } ) - }.fix() + } } } -interface GetArticlesUseCase { - val getArticles: GetArticles - val getArticlesCount: GetArticlesCount +interface GetArticlesUseCase { + val getArticles: GetArticles + val getArticlesCount: GetArticlesCount - fun GetArticlesCommand.runUseCase(): IO, Long>> { + fun GetArticlesCommand.runUseCase(MD: MonadDefer): Kind, Long>> { val cmd = this - return IO.monad().binding { + return MD.binding { val count = getArticlesCount(cmd.filter).bind() if (count == 0L) Pair(listOf(), 0L) @@ -124,7 +120,7 @@ interface GetArticlesUseCase { val articles = getArticles(cmd.filter, cmd.user).bind() Pair(articles, count) } - }.fix() + } } } @@ -145,26 +141,28 @@ interface GetFeedsUseCase { } } -interface UpdateArticleUseCase { - val validateUpdate: ValidateArticleUpdate - val updateArticle: UpdateArticle +interface UpdateArticleUseCase { + val validateUpdate: ValidateArticleUpdate + val updateArticle: UpdateArticle - fun UpdateArticleCommand.runUseCase(): IO> { + fun UpdateArticleCommand.runUseCase(MD: MonadDefer): Kind> { val cmd = this - return EitherT.monad(IO.monad()).binding { + return EitherT.monad(MD).binding { val validUpdate = EitherT(validateUpdate(cmd.data, cmd.slug, cmd.user)).bind() - EitherT(updateArticle(validUpdate, cmd.user).map { it.right() }).bind() - }.value().fix() + EitherT( + MD.run { updateArticle(validUpdate, cmd.user).map { it.right() } } + ).bind() + }.value() } } -interface FavoriteUseCase { - val getArticleBySlug: GetArticleBySlug - val addFavorite: AddFavorite +interface FavoriteUseCase { + val getArticleBySlug: GetArticleBySlug + val addFavorite: AddFavorite - fun FavoriteArticleCommand.runUseCase(): IO> { + fun FavoriteArticleCommand.runUseCase(MD: MonadDefer): Kind> { val cmd = this - return IO.monad().binding { + return MD.binding { getArticleBySlug(cmd.slug, cmd.user.some()).bind().fold( { ArticleFavoriteError.NotFound.left() }, { @@ -180,17 +178,17 @@ interface FavoriteUseCase { } } ) - }.fix() + } } } -interface UnfavoriteUseCase { - val getArticleBySlug: GetArticleBySlug - val removeFavorite: RemoveFavorite +interface UnfavoriteUseCase { + val getArticleBySlug: GetArticleBySlug + val removeFavorite: RemoveFavorite - fun UnfavoriteArticleCommand.runUseCase(): IO> { + fun UnfavoriteArticleCommand.runUseCase(MD: MonadDefer): Kind> { val cmd = this - return IO.monad().binding { + return MD.binding { getArticleBySlug(cmd.slug, cmd.user.some()).bind().fold( { ArticleUnfavoriteError.NotFound.left() }, { @@ -204,33 +202,33 @@ interface UnfavoriteUseCase { } } ) - }.fix() + } } } -interface CommentUseCase { - val getArticleBySlug: GetArticleBySlug - val addComment: AddComment +interface CommentUseCase { + val getArticleBySlug: GetArticleBySlug + val addComment: AddComment - fun CommentArticleCommand.runUsecase(): IO> { + fun CommentArticleCommand.runUsecase(MD: MonadDefer): Kind> { val cmd = this - return IO.monad().binding { + return MD.binding { getArticleBySlug(cmd.slug, cmd.user.some()).bind().fold( { ArticleCommentError.NotFound.left() }, { addComment(it.id, cmd.comment, cmd.user).bind().right() } ) - }.fix() + } } } -interface DeleteCommentUseCase { - val getArticleBySlug: GetArticleBySlug - val getComment: GetComment - val deleteComment: DeleteComment +interface DeleteCommentUseCase { + val getArticleBySlug: GetArticleBySlug + val getComment: GetComment + val deleteComment: DeleteComment - fun DeleteCommentCommand.runUseCase(): IO> { + fun DeleteCommentCommand.runUseCase(MD: MonadDefer): Kind> { val cmd = this - return IO.monad().binding { + return MD.binding { getArticleBySlug(cmd.slug, cmd.user.some()).bind().fold( { ArticleCommentDeleteError.ArticleNotFound.left() }, { @@ -246,29 +244,29 @@ interface DeleteCommentUseCase { ) } ) - }.fix() + } } } -interface GetCommentsUseCase { - val getArticleBySlug: GetArticleBySlug - val getComments: GetComments +interface GetCommentsUseCase { + val getArticleBySlug: GetArticleBySlug + val getComments: GetComments - fun GetCommentsCommand.runUseCase(): IO>> { + fun GetCommentsCommand.runUseCase(MD: MonadDefer): Kind>> { val cmd = this - return IO.monad().binding { + return MD.binding { getArticleBySlug(cmd.slug, cmd.user).bind().fold( { none>() }, { getComments(it.id, cmd.user).bind().some() } ) - }.fix() + } } } -interface GetTagsUseCase { - val getTags: GetTags +interface GetTagsUseCase { + val getTags: GetTags - fun GetTagsCommand.runUseCase(): IO> = getTags() + fun GetTagsCommand.runUseCase(): Kind> = getTags() } private fun Option
.getOrSystemError(slug: String) = this.getOrElse { diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/Dependencies.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/Dependencies.kt index 09596ad..6a89015 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/Dependencies.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/Dependencies.kt @@ -1,11 +1,11 @@ package io.realworld.domain.profiles +import arrow.Kind import arrow.core.Option -import arrow.effects.IO import io.realworld.domain.users.User import io.realworld.domain.users.UserId -typealias GetUserByUsername = (username: String) -> IO> -typealias HasFollower = (followee: UserId, follower: UserId) -> IO -typealias AddFollower = (followee: UserId, follower: UserId) -> IO -typealias RemoveFollower = (followee: UserId, follower: UserId) -> IO +typealias GetUserByUsername = (username: String) -> Kind> +typealias HasFollower = (followee: UserId, follower: UserId) -> Kind +typealias AddFollower = (followee: UserId, follower: UserId) -> Kind +typealias RemoveFollower = (followee: UserId, follower: UserId) -> Kind diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/UseCases.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/UseCases.kt index a8869e9..5990b11 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/UseCases.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/UseCases.kt @@ -1,12 +1,11 @@ package io.realworld.domain.profiles +import arrow.Kind import arrow.core.Option import arrow.core.none import arrow.core.some import arrow.core.toOption -import arrow.effects.IO -import arrow.effects.fix -import arrow.effects.instances.io.monad.monad +import arrow.effects.typeclasses.MonadDefer import arrow.typeclasses.binding import io.realworld.domain.users.User @@ -14,13 +13,13 @@ data class GetProfileCommand(val username: String, val current: Option) data class FollowCommand(val username: String, val current: User) data class UnfollowCommand(val username: String, val current: User) -interface GetProfileUseCase { - val getUser: GetUserByUsername - val hasFollower: HasFollower +interface GetProfileUseCase { + val getUser: GetUserByUsername + val hasFollower: HasFollower - fun GetProfileCommand.runUseCase(): IO> { + fun GetProfileCommand.runUseCase(MD: MonadDefer): Kind> { val cmd = this - return IO.monad().binding { + return MD.binding { getUser(cmd.username).bind().fold( { none() }, { @@ -35,17 +34,17 @@ interface GetProfileUseCase { ).some() } ) - }.fix() + } } } -interface FollowUseCase { - val getUser: GetUserByUsername - val addFollower: AddFollower +interface FollowUseCase { + val getUser: GetUserByUsername + val addFollower: AddFollower - fun FollowCommand.runUseCase(): IO> { + fun FollowCommand.runUseCase(MD: MonadDefer): Kind> { val cmd = this - return IO.monad().binding { + return MD.binding { getUser(cmd.username).bind().fold( { none() }, { @@ -58,17 +57,17 @@ interface FollowUseCase { ).some() } ) - }.fix() + } } } -interface UnfollowUseCase { - val getUser: GetUserByUsername - val removeFollower: RemoveFollower +interface UnfollowUseCase { + val getUser: GetUserByUsername + val removeFollower: RemoveFollower - fun UnfollowCommand.runUseCase(): IO> { + fun UnfollowCommand.runUseCase(MD: MonadDefer): Kind> { val cmd = this - return IO.monad().binding { + return MD.binding { getUser(cmd.username).bind().fold( { none() }, { @@ -81,6 +80,6 @@ interface UnfollowUseCase { ).some() } ) - }.fix() + } } } diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/users/Dependencies.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/users/Dependencies.kt index 75ed2c5..527905e 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/users/Dependencies.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/users/Dependencies.kt @@ -3,15 +3,16 @@ package io.realworld.domain.users import arrow.Kind import arrow.core.Either import arrow.core.Option -import arrow.effects.IO -typealias ValidateUserRegistration = (reg: UserRegistration) -> IO> -typealias CreateUser = (user: ValidUserRegistration) -> IO +typealias ValidateUserRegistration = + (reg: UserRegistration) -> Kind> +typealias CreateUser = (user: ValidUserRegistration) -> Kind -typealias ValidateUserUpdate = (update: UserUpdate, current: User) -> IO> -typealias UpdateUser = (update: ValidUserUpdate, current: User) -> IO +typealias ValidateUserUpdate = + (update: UserUpdate, current: User) -> Kind> +typealias UpdateUser = (update: ValidUserUpdate, current: User) -> Kind typealias GetUserByEmail = (email: String) -> Kind> -typealias ExistsByEmail = (email: String) -> IO -typealias ExistsByUsername = (username: String) -> IO +typealias ExistsByEmail = (email: String) -> Kind +typealias ExistsByUsername = (username: String) -> Kind diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/users/Services.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/users/Services.kt index 5b2b585..3244003 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/users/Services.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/users/Services.kt @@ -1,25 +1,25 @@ package io.realworld.domain.users +import arrow.Kind import arrow.core.Either import arrow.core.getOrElse import arrow.core.left import arrow.core.right -import arrow.effects.IO -import arrow.effects.fix -import arrow.effects.instances.io.monad.monad +import arrow.effects.typeclasses.MonadDefer import arrow.typeclasses.binding import io.realworld.domain.common.Auth import io.realworld.domain.common.Token import java.util.UUID -interface ValidateUserService { +interface ValidateUserService { val auth: Auth - val existsByUsername: ExistsByUsername - val existsByEmail: ExistsByEmail + val existsByUsername: ExistsByUsername + val existsByEmail: ExistsByEmail + val MD: MonadDefer - fun UserRegistration.validate(): IO> { + fun UserRegistration.validate(): Kind> { val cmd = this - return IO.monad().binding { + return MD.binding { when { existsByEmail(cmd.email).bind() -> UserRegistrationError.EmailAlreadyTaken.left() @@ -36,18 +36,19 @@ interface ValidateUserService { ).right() } } - }.fix() + } } } -interface ValidateUserUpdateService { +interface ValidateUserUpdateService { val auth: Auth - val existsByUsername: ExistsByUsername - val existsByEmail: ExistsByEmail + val existsByUsername: ExistsByUsername + val existsByEmail: ExistsByEmail + val MD: MonadDefer - fun UserUpdate.validate(current: User): IO> { + fun UserUpdate.validate(current: User): Kind> { val cmd = this - return IO.monad().binding { + return MD.binding { when { cmd.email.fold({ false }, { current.email !== it && existsByEmail(it).bind() }) -> UserUpdateError.EmailAlreadyTaken.left() @@ -61,6 +62,6 @@ interface ValidateUserUpdateService { image = image.getOrElse { current.image } ).right() } - }.fix() + } } } diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt index 6c211e6..d254234 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt @@ -6,10 +6,6 @@ import arrow.core.left import arrow.core.right import arrow.data.EitherT import arrow.data.value -import arrow.effects.ForIO -import arrow.effects.IO -import arrow.effects.fix -import arrow.effects.instances.io.monad.monad import arrow.effects.typeclasses.MonadDefer import arrow.instances.monad import arrow.typeclasses.binding @@ -32,17 +28,19 @@ sealed class UserUpdateError { object UsernameAlreadyTaken : UserUpdateError() } -interface RegisterUserUseCase { +interface RegisterUserUseCase { val auth: Auth - val createUser: CreateUser - val validateUser: ValidateUserRegistration + val createUser: CreateUser + val validateUser: ValidateUserRegistration - fun RegisterUserCommand.runUseCase(): IO> { + fun RegisterUserCommand.runUseCase(MD: MonadDefer): Kind> { val cmd = this - return EitherT.monad(IO.monad()).binding { + return EitherT.monad(MD).binding { val validRegistration = EitherT(validateUser(cmd.data)).bind() - EitherT(createUser(validRegistration).map { it.right() }).bind() - }.value().fix() + EitherT( + MD.run { createUser(validRegistration).map { it.right() } } + ).bind() + }.value() } } @@ -69,16 +67,18 @@ interface LoginUserUseCase { } } -interface UpdateUserUseCase { +interface UpdateUserUseCase { val auth: Auth - val validateUpdate: ValidateUserUpdate - val updateUser: UpdateUser + val validateUpdate: ValidateUserUpdate + val updateUser: UpdateUser - fun UpdateUserCommand.runUseCase(): IO> { + fun UpdateUserCommand.runUseCase(MD: MonadDefer): Kind> { val cmd = this - return EitherT.monad(IO.monad()).binding { + return EitherT.monad(MD).binding { val validUpdate = EitherT(validateUpdate(cmd.data, cmd.current)).bind() - EitherT(updateUser(validUpdate, current).map { it.right() }).bind() - }.value().fix() + EitherT( + MD.run { updateUser(validUpdate, current).map { it.right() } } + ).bind() + }.value() } } diff --git a/realworld-domain/src/test/kotlin/io/realworld/domain/users/UsersTest.kt b/realworld-domain/src/test/kotlin/io/realworld/domain/users/UsersTest.kt index cfe7fbd..fb902a1 100644 --- a/realworld-domain/src/test/kotlin/io/realworld/domain/users/UsersTest.kt +++ b/realworld-domain/src/test/kotlin/io/realworld/domain/users/UsersTest.kt @@ -2,7 +2,10 @@ package io.realworld.domain.users import arrow.core.right +import arrow.effects.ForIO import arrow.effects.IO +import arrow.effects.fix +import arrow.effects.instances.io.monadDefer.monadDefer import arrow.effects.liftIO import io.realworld.domain.common.Auth import io.realworld.domain.common.Settings @@ -22,13 +25,13 @@ class RegisterUserWorkflowTests { "foo", "foo@bar.com", "bar" ) - val createUser0: CreateUser = { x -> + val createUser0: CreateUser = { x -> User(id = UUID.randomUUID().userId(), email = x.email, token = x.token, username = x.username).liftIO() } @Test fun `happy path`() { - val actual = object : RegisterUserUseCase { + val actual = object : RegisterUserUseCase { override val auth = auth0 override val createUser = createUser0 override val validateUser = { x: UserRegistration -> x.autovalid().right().liftIO() } @@ -40,18 +43,18 @@ class RegisterUserWorkflowTests { @Test fun `exceptions from dependencies are propagated`() { assertThatThrownBy { - object : RegisterUserUseCase { + object : RegisterUserUseCase { override val auth = auth0 - override val createUser: CreateUser = { _ -> IO.raiseError(RuntimeException("BOOM!")) } + override val createUser: CreateUser = { _ -> IO.raiseError(RuntimeException("BOOM!")) } override val validateUser = { x: UserRegistration -> x.autovalid().right().liftIO() } }.test(userRegistration).unsafeRunSync() }.hasMessage("BOOM!") assertThatThrownBy { - object : RegisterUserUseCase { + object : RegisterUserUseCase { override val auth = auth0 override val createUser = createUser0 - override val validateUser: ValidateUserRegistration = { _ -> IO.raiseError(RuntimeException("BOOM!")) } + override val validateUser: ValidateUserRegistration = { _ -> IO.raiseError(RuntimeException("BOOM!")) } }.test(userRegistration).unsafeRunSync() }.hasMessage("BOOM!") } @@ -61,9 +64,9 @@ class RegisterUserWorkflowTests { var userSaved = false catchThrowable { - object : RegisterUserUseCase { + object : RegisterUserUseCase { override val auth = auth0 - override val createUser: CreateUser = { x -> + override val createUser: CreateUser = { x -> IO { userSaved = true User(id = UUID.randomUUID().userId(), email = x.email, token = x.token, username = x.username) @@ -75,8 +78,8 @@ class RegisterUserWorkflowTests { assertThat(userSaved).isFalse() } - private fun RegisterUserUseCase.test(input: UserRegistration) = this.run { - RegisterUserCommand(input).runUseCase() + private fun RegisterUserUseCase.test(input: UserRegistration) = this.run { + RegisterUserCommand(input).runUseCase(IO.monadDefer()).fix() } private fun UserRegistration.autovalid() = UUID.randomUUID().userId().let { diff --git a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt index 7b21945..cdc44c7 100644 --- a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt +++ b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt @@ -5,7 +5,6 @@ import arrow.core.Option import arrow.core.getOrElse import arrow.core.some import arrow.core.toOption -import arrow.effects.IO import arrow.effects.typeclasses.MonadDefer import io.realworld.domain.articles.Article import io.realworld.domain.articles.ArticleFilter @@ -114,7 +113,7 @@ class ArticleRepository( MD: MonadDefer ) : MonadDefer by MD { - fun create(article: ValidArticleCreation, user: User): IO
= IO { + fun create(article: ValidArticleCreation, user: User): Kind = defer { val row = insertArticleRow(article, user) val deps = ArticleDeps() @@ -131,84 +130,86 @@ class ArticleRepository( deps.tagList.addAll(article.tagList) } - Article.from(row, deps) + Article.from(row, deps).just() } - fun existsBySlug(slug: String): IO = ArticleTbl.let { - jdbcTemplate.queryIfExists(it.table, "${it.slug.eq()}", mapOf(it.slug to slug)) + fun existsBySlug(slug: String): Kind = defer { + ArticleTbl.let { + jdbcTemplate.queryIfExists(it.table, "${it.slug.eq()}", mapOf(it.slug to slug)).just() + } } - fun getBySlug(slug: String, user: Option): IO> = IO { - fetchRowBySlug(slug).map { Article.from(it, loadArticleDeps(it, user)) } + fun getBySlug(slug: String, user: Option): Kind> = defer { + fetchRowBySlug(slug).map { Article.from(it, loadArticleDeps(it, user)) }.just() } - fun deleteArticle(articleId: ArticleId): IO = with(ArticleTbl) { + fun deleteArticle(articleId: ArticleId): Kind = with(ArticleTbl) { val sql = "DELETE FROM $table WHERE ${id.eq()}" val params = mapOf(id to articleId.value) - IO { - jdbcTemplate.update(sql, params) + defer { + jdbcTemplate.update(sql, params).just() } } - fun updateArticle(update: ValidArticleUpdate, user: User): IO
= IO { + fun updateArticle(update: ValidArticleUpdate, user: User): Kind = defer { val row = updateArticleRow(update) - Article.from(row, loadArticleDeps(row, user.some())) + Article.from(row, loadArticleDeps(row, user.some())).just() } - fun addFavorite(articleId: ArticleId, user: User): IO = with(ArticleFavoriteTbl) { + fun addFavorite(articleId: ArticleId, user: User): Kind = with(ArticleFavoriteTbl) { val sql = "${table.insert(article_id, user_id)} ON CONFLICT ($article_id, $user_id) DO NOTHING" val params = mapOf(article_id to articleId.value, user_id to user.id.value) - IO { - jdbcTemplate.update(sql, params) + defer { + jdbcTemplate.update(sql, params).just() } } - fun removeFavorite(articleId: ArticleId, user: User): IO = with(ArticleFavoriteTbl) { + fun removeFavorite(articleId: ArticleId, user: User): Kind = with(ArticleFavoriteTbl) { val sql = "DELETE FROM $table WHERE ${article_id.eq()} AND ${user_id.eq()}" val params = mapOf(article_id to articleId.value, user_id to user.id.value) - IO { - jdbcTemplate.update(sql, params) + defer { + jdbcTemplate.update(sql, params).just() } } - fun getComment(commentId: Long, user: User): IO> = IO { + fun getComment(commentId: Long, user: User): Kind> = defer { fetchCommentRowById(commentId).map { Comment.from(it, CommentDeps(fetchAuthor(it.authorId, user.some()))) - } + }.just() } - fun addComment(articleId: ArticleId, comment: String, user: User): IO = IO { + fun addComment(articleId: ArticleId, comment: String, user: User): Kind = defer { val row = insertCommentRow(articleId, comment, user) val deps = CommentDeps(fetchAuthor(row.authorId, user.some())) - Comment.from(row, deps) + Comment.from(row, deps).just() } - fun deleteComment(commentId: Long): IO = with(ArticleCommentTbl) { + fun deleteComment(commentId: Long): Kind = with(ArticleCommentTbl) { val sql = "DELETE FROM $table WHERE ${id.eq()}" val params = mapOf(id to commentId) - IO { - jdbcTemplate.update(sql, params) + defer { + jdbcTemplate.update(sql, params).just() } } - fun getComments(articleId: ArticleId, user: Option): IO> = with(ArticleCommentTbl) { + fun getComments(articleId: ArticleId, user: Option): Kind> = with(ArticleCommentTbl) { val sql = "SELECT * from $table WHERE ${article_id.eq()}" val params = mapOf(article_id to articleId.value) - IO { + defer { jdbcTemplate.query(sql, params) { rs, _ -> CommentRow.fromRs(rs) }.map { Comment.from(it, CommentDeps(fetchAuthor(it.authorId, user))) - } + }.just() } } - fun getArticles(filter: ArticleFilter, user: Option): IO> = IO { + fun getArticles(filter: ArticleFilter, user: Option): Kind> = defer { val rows = fetchArticleRows(filter.toQueryParts(), filter.limit, filter.offset) // NOTE: opt for simplicity (query limit defaults to 20), thus let's loop - rows.map { row -> Article.from(row, loadArticleDeps(row, user)) } + rows.map { row -> Article.from(row, loadArticleDeps(row, user)) }.just() } - fun getArticlesCount(filter: ArticleFilter): IO = IO { - fetchArticleRowCount(filter.toQueryParts()) + fun getArticlesCount(filter: ArticleFilter): Kind = defer { + fetchArticleRowCount(filter.toQueryParts()).just() } fun getFeeds(filter: FeedFilter, user: User): Kind> = defer { @@ -221,15 +222,15 @@ class ArticleRepository( fetchArticleRowCount(user.toFeedsQueryParts()).just() } - fun getTags(): IO> = with(TagTbl) { - IO { - jdbcTemplate.query("SELECT $name FROM $table") { rs, _ -> rs.getString(name) }.toSet() + fun getTags(): Kind> = with(TagTbl) { + defer { + jdbcTemplate.query("SELECT $name FROM $table") { rs, _ -> rs.getString(name) }.toSet().just() } } private fun loadArticleDeps(row: ArticleRow, user: Option): ArticleDeps = ArticleDeps().apply { - favorited = user.map { isFavorited(row.id, it).unsafeRunSync() }.getOrElse { false } + favorited = user.map { isFavorited(row.id, it) }.getOrElse { false } favoritesCount = fetchFavoritesCount(row.id) tagList.addAll(fetchArticleTags(row.id)) author = fetchAuthor(row.authorId, user) @@ -376,13 +377,13 @@ class ArticleRepository( } private fun fetchAuthor(id: UserId, querier: Option): Profile = - userRepo.findById(id).unsafeRunSync().map { + userRepo.findByIdEff(id).map { it.user.let { author -> Profile( username = author.username, bio = author.bio.toOption(), image = author.image.toOption(), - following = querier.map { userRepo.hasFollower(author.id, it.id).unsafeRunSync() } + following = querier.map { userRepo.hasFollowerEff(author.id, it.id) } ) } }.getOrElse { throw RuntimeException("Corrupt DB: article author $id not found") } @@ -393,7 +394,7 @@ class ArticleRepository( jdbcTemplate.queryForObject(sql, params, { rs, _ -> rs.getLong("count") })!! } - private fun isFavorited(articleId: ArticleId, user: User): IO = with(ArticleFavoriteTbl) { + private fun isFavorited(articleId: ArticleId, user: User): Boolean = with(ArticleFavoriteTbl) { jdbcTemplate.queryIfExists( table, "${article_id.eq()} AND ${user_id.eq()}", diff --git a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/QueryUtils.kt b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/QueryUtils.kt index 4390dee..c52187f 100644 --- a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/QueryUtils.kt +++ b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/QueryUtils.kt @@ -1,6 +1,5 @@ package io.realworld.persistence -import arrow.effects.IO import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate object Dsl { @@ -14,10 +13,7 @@ internal fun NamedParameterJdbcTemplate.queryIfExists( table: String, where: String, params: Map -): IO = IO { - this.queryForObject( - "SELECT COUNT(*) FROM $table WHERE $where", - params, - { rs, _ -> rs.getInt("count") > 0 } - )!! -} +): Boolean = this.queryForObject( + "SELECT COUNT(*) FROM $table WHERE $where", + params +) { rs, _ -> rs.getInt("count") > 0 }!! diff --git a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt index 73a5dc2..1d1c79c 100644 --- a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt +++ b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt @@ -3,7 +3,6 @@ package io.realworld.persistence import arrow.Kind import arrow.core.Option import arrow.core.toOption -import arrow.effects.IO import arrow.effects.typeclasses.MonadDefer import io.realworld.domain.users.User import io.realworld.domain.users.UserAndPassword @@ -39,7 +38,7 @@ open class UserRepository( UserAndPassword(User.fromRs(rs), rs.getString(password)) } - fun create(user: ValidUserRegistration): IO { + fun create(user: ValidUserRegistration): Kind { val sql = with(UserTbl) { """ INSERT INTO $table ( @@ -59,12 +58,12 @@ open class UserRepository( password to user.encryptedPassword ) } - return IO { - jdbcTemplate.queryForObject(sql, params, { rs, _ -> User.fromRs(rs) })!! + return defer { + jdbcTemplate.queryForObject(sql, params) { rs, _ -> User.fromRs(rs) }!!.just() } } - fun update(update: ValidUserUpdate, current: User): IO { + fun update(update: ValidUserUpdate, current: User): Kind { val sql = with(UserTbl) { StringBuilder("UPDATE $table SET ${username.set()}, ${email.set()}, ${bio.set()}, ${image.set()}") .also { if (update.encryptedPassword.isDefined()) it.append(", ${password.set()}") } @@ -82,21 +81,22 @@ open class UserRepository( ) } - return IO { - jdbcTemplate.queryForObject(sql, params, { rs, _ -> User.fromRs(rs) })!! + return defer { + jdbcTemplate.queryForObject(sql, params) { rs, _ -> User.fromRs(rs) }!!.just() } } - fun findById(id: UserId): IO> = - IO { - DataAccessUtils.singleResult( - jdbcTemplate.query( - "SELECT * FROM ${UserTbl.table} WHERE ${UserTbl.id.eq()}", - mapOf(UserTbl.id to id.value), - { rs, _ -> UserAndPassword.fromRs(rs) } - ) - ).toOption() - } + fun findById(id: UserId): Kind> = + defer { findByIdEff(id).just() } + + fun findByIdEff(id: UserId): Option = + DataAccessUtils.singleResult( + jdbcTemplate.query( + "SELECT * FROM ${UserTbl.table} WHERE ${UserTbl.id.eq()}", + mapOf(UserTbl.id to id.value), + { rs, _ -> UserAndPassword.fromRs(rs) } + ) + ).toOption() fun findByEmail(email: String): Kind> = defer { @@ -108,26 +108,34 @@ open class UserRepository( ).toOption().just() } - fun findByUsername(username: String): IO> = - IO { + fun findByUsername(username: String): Kind> = + defer { DataAccessUtils.singleResult( jdbcTemplate.query( "SELECT * FROM ${UserTbl.table} WHERE ${UserTbl.username.eq()}", mapOf(UserTbl.username to username), { rs, _ -> User.fromRs(rs) } ) - ).toOption() + ).toOption().just() } - open fun existsByEmail(email: String): IO = UserTbl.let { - jdbcTemplate.queryIfExists(it.table, "${it.email.eq()}", mapOf(it.email to email)) + open fun existsByEmail(email: String): Kind = defer { + UserTbl.let { + jdbcTemplate.queryIfExists(it.table, "${it.email.eq()}", mapOf(it.email to email)) + }.just() + } + + fun existsByUsername(username: String): Kind = defer { + UserTbl.let { + jdbcTemplate.queryIfExists(it.table, "${it.username.eq()}", mapOf(it.username to username)) + }.just() } - fun existsByUsername(username: String): IO = UserTbl.let { - jdbcTemplate.queryIfExists(it.table, "${it.username.eq()}", mapOf(it.username to username)) + fun hasFollower(followee: UserId, follower: UserId): Kind = defer { + hasFollowerEff(followee, follower).just() } - fun hasFollower(followee: UserId, follower: UserId): IO = FollowTbl.let { + fun hasFollowerEff(followee: UserId, follower: UserId): Boolean = FollowTbl.let { jdbcTemplate.queryIfExists( it.table, "${it.followee.eq()} AND ${it.follower.eq()}", @@ -135,19 +143,19 @@ open class UserRepository( ) } - fun addFollower(followee: UserId, follower: UserId): IO = FollowTbl.let { + fun addFollower(followee: UserId, follower: UserId): Kind = FollowTbl.let { val sql = "${it.table.insert(it.followee, it.follower)} ON CONFLICT (${it.followee}, ${it.follower}) DO NOTHING" val params = mapOf(it.followee to followee.value, it.follower to follower.value) - IO { - jdbcTemplate.update(sql, params) + defer { + jdbcTemplate.update(sql, params).just() } } - fun removeFollower(followee: UserId, follower: UserId): IO = FollowTbl.let { + fun removeFollower(followee: UserId, follower: UserId): Kind = FollowTbl.let { val sql = "DELETE FROM ${it.table} WHERE ${it.followee.eq()} AND ${it.follower.eq()}" val params = mapOf(it.followee to followee.value, it.follower to follower.value) - IO { - jdbcTemplate.update(sql, params) + defer { + jdbcTemplate.update(sql, params).just() } } } From 506a166b5178829664425a9059abd99f671fc17b Mon Sep 17 00:00:00 2001 From: Isto Nikula Date: Sun, 13 Jan 2019 05:03:22 +0200 Subject: [PATCH 05/10] refactor(use-cases): provide MonadDefer instance consistently as dep --- .../kotlin/io/realworld/articles/Routes.kt | 30 ++++++++++++------- .../kotlin/io/realworld/profiles/Routes.kt | 9 ++++-- .../main/kotlin/io/realworld/users/Routes.kt | 9 ++++-- .../io/realworld/domain/articles/UseCases.kt | 30 ++++++++++++------- .../io/realworld/domain/profiles/UseCases.kt | 9 ++++-- .../io/realworld/domain/users/UseCases.kt | 9 ++++-- .../io/realworld/domain/users/UsersTest.kt | 12 +++++--- 7 files changed, 72 insertions(+), 36 deletions(-) diff --git a/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt b/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt index f41e86f..c9525a5 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt @@ -115,11 +115,12 @@ class ArticleController( return object : CreateArticleUseCase { override val createUniqueSlug = createUniqueSlugSrv::slufigy override val createArticle = articleRepo::create + override val MD = ioMonadDefer }.run { CreateArticleCommand( data = dto.toDomain(), user = user - ).runUseCase(ioMonadDefer) + ).runUseCase() }.fix().runWriteTx(txManager).let { ResponseEntity.status(HttpStatus.CREATED).body(ArticleResponse.fromDomain(it)) } @@ -140,8 +141,9 @@ class ArticleController( return object : GetArticlesUseCase { override val getArticles = articleRepo::getArticles override val getArticlesCount = articleRepo::getArticlesCount + override val MD = IO.monadDefer() }.run { - GetArticlesCommand(filter, user).runUseCase(IO.monadDefer()) + GetArticlesCommand(filter, user).runUseCase() }.fix().runReadTx(txManager).let { ResponseEntity.ok(ArticlesResponse.fromDomain(it)) } @@ -155,8 +157,9 @@ class ArticleController( return object : GetFeedsUseCase { override val getFeeds = articleRepo::getFeeds override val getFeedsCount = articleRepo::getFeedsCount + override val MD = IO.monadDefer() }.run { - GetFeedsCommand(filter, user).runUseCase(IO.monadDefer()) + GetFeedsCommand(filter, user).runUseCase() }.fix().runReadTx(txManager).let { ResponseEntity.ok(ArticlesResponse.fromDomain(it)) } @@ -192,8 +195,9 @@ class ArticleController( return object : DeleteArticleUseCase { override val getArticleBySlug = articleRepo::getBySlug override val deleteArticle = articleRepo::deleteArticle + override val MD = IO.monadDefer() }.run { - DeleteArticleCommand(slug, user).runUseCase(IO.monadDefer()) + DeleteArticleCommand(slug, user).runUseCase() }.fix().runWriteTx(txManager).fold( { when (it) { @@ -227,8 +231,9 @@ class ArticleController( override val validateUpdate: ValidateArticleUpdate = { x, y, z -> validateUpdateSrv.run { x.validate(y, z) } } override val updateArticle = articleRepo::updateArticle + override val MD = ioMonadDefer }.run { - UpdateArticleCommand(update.toDomain(), slug, user).runUseCase(ioMonadDefer) + UpdateArticleCommand(update.toDomain(), slug, user).runUseCase() }.fix().runWriteTx(txManager).fold( { when (it) { @@ -249,8 +254,9 @@ class ArticleController( return object : FavoriteUseCase { override val getArticleBySlug = articleRepo::getBySlug override val addFavorite = articleRepo::addFavorite + override val MD = IO.monadDefer() }.run { - FavoriteArticleCommand(slug, user).runUseCase(IO.monadDefer()) + FavoriteArticleCommand(slug, user).runUseCase() }.fix().runWriteTx(txManager).fold( { when (it) { @@ -270,8 +276,9 @@ class ArticleController( return object : UnfavoriteUseCase { override val getArticleBySlug = articleRepo::getBySlug override val removeFavorite = articleRepo::removeFavorite + override val MD = IO.monadDefer() }.run { - UnfavoriteArticleCommand(slug, user).runUseCase(IO.monadDefer()) + UnfavoriteArticleCommand(slug, user).runUseCase() }.fix().runWriteTx(txManager).fold( { when (it) { @@ -297,8 +304,9 @@ class ArticleController( return object : GetCommentsUseCase { override val getArticleBySlug = articleRepo::getBySlug override val getComments = articleRepo::getComments + override val MD = IO.monadDefer() }.run { - GetCommentsCommand(slug, user).runUseCase(IO.monadDefer()) + GetCommentsCommand(slug, user).runUseCase() }.fix().runReadTx(txManager).fold( { ResponseEntity.notFound().build() }, { ResponseEntity.ok(CommentsResponse.fromDomain(it)) } @@ -314,8 +322,9 @@ class ArticleController( return object : CommentUseCase { override val getArticleBySlug = articleRepo::getBySlug override val addComment = articleRepo::addComment + override val MD = IO.monadDefer() }.run { - CommentArticleCommand(slug, comment.body, user).runUsecase(IO.monadDefer()) + CommentArticleCommand(slug, comment.body, user).runUsecase() }.fix().runWriteTx(txManager).fold( { when (it) { @@ -336,8 +345,9 @@ class ArticleController( override val getArticleBySlug = articleRepo::getBySlug override val deleteComment = articleRepo::deleteComment override val getComment = articleRepo::getComment + override val MD = IO.monadDefer() }.run { - DeleteCommentCommand(slug, commentId, user).runUseCase(IO.monadDefer()) + DeleteCommentCommand(slug, commentId, user).runUseCase() }.fix().runWriteTx(txManager).fold( { when (it) { diff --git a/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt b/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt index c102e88..53350b7 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt @@ -55,8 +55,9 @@ class ProfileController( return object : GetProfileUseCase { override val getUser = repo::findByUsername override val hasFollower = repo::hasFollower + override val MD = IO.monadDefer() }.run { - GetProfileCommand(username, user).runUseCase(IO.monadDefer()) + GetProfileCommand(username, user).runUseCase() }.fix().runReadTx(txManager).fold( { ResponseEntity.notFound().build() }, { ResponseEntity.ok(ProfileResponse.fromDomain(it)) } @@ -71,8 +72,9 @@ class ProfileController( return object : FollowUseCase { override val addFollower = repo::addFollower override val getUser = repo::findByUsername + override val MD = IO.monadDefer() }.run { - FollowCommand(username, current).runUseCase(IO.monadDefer()) + FollowCommand(username, current).runUseCase() }.fix().runWriteTx(txManager).fold( { ResponseEntity.notFound().build() }, { ResponseEntity.ok(ProfileResponse.fromDomain(it)) } @@ -87,8 +89,9 @@ class ProfileController( return object : UnfollowUseCase { override val getUser = repo::findByUsername override val removeFollower = repo::removeFollower + override val MD = IO.monadDefer() }.run { - UnfollowCommand(username, current).runUseCase(IO.monadDefer()) + UnfollowCommand(username, current).runUseCase() }.fix().runWriteTx(txManager).fold( { ResponseEntity.notFound().build() }, { ResponseEntity.ok(ProfileResponse.fromDomain(it)) } diff --git a/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt b/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt index 7747d89..45bb097 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt @@ -68,12 +68,13 @@ class UserController( override val auth = auth0 override val createUser: CreateUser = repo::create override val validateUser: ValidateUserRegistration = { x -> validateUserSrv.run { x.validate() } } + override val MD = ioMonadDefer }.run { RegisterUserCommand(UserRegistration( username = registration.username, email = registration.email, password = registration.password - )).runUseCase(ioMonadDefer) + )).runUseCase() }.fix().runWriteTx(txManager).fold( { when (it) { @@ -92,11 +93,12 @@ class UserController( return object : LoginUserUseCase { override val auth = auth0 override val getUser: GetUserByEmail = repo::findByEmail + override val MD = IO.monadDefer() }.run { LoginUserCommand( email = login.email, password = login.password - ).runUseCase(IO.monadDefer()) + ).runUseCase() }.fix().runWriteTx(txManager).fold( { throw UnauthorizedException() }, { ResponseEntity.ok().body(UserResponse.fromDomain(it)) } @@ -117,6 +119,7 @@ class UserController( override val auth = auth0 override val validateUpdate: ValidateUserUpdate = { x, y -> validateUpdateSrv.run { x.validate(y) } } override val updateUser: UpdateUser = repo::update + override val MD = ioMonadDefer }.run { UpdateUserCommand( data = UserUpdate( @@ -127,7 +130,7 @@ class UserController( image = Option.fromNullable(update.image) ), current = user - ).runUseCase(ioMonadDefer) + ).runUseCase() }.fix().runWriteTx(txManager).fold( { when (it) { diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt index 354894f..26baf0f 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt @@ -61,8 +61,9 @@ sealed class ArticleCommentDeleteError { interface CreateArticleUseCase { val createUniqueSlug: CreateUniqueSlug val createArticle: CreateArticle + val MD: MonadDefer - fun CreateArticleCommand.runUseCase(MD: MonadDefer): Kind { + fun CreateArticleCommand.runUseCase(): Kind { val cmd = this return MD.binding { val slug = createUniqueSlug(cmd.data.title).bind() @@ -91,8 +92,9 @@ interface GetArticleUseCase { interface DeleteArticleUseCase { val getArticleBySlug: GetArticleBySlug val deleteArticle: DeleteArticle + val MD: MonadDefer - fun DeleteArticleCommand.runUseCase(MD: MonadDefer): Kind> { + fun DeleteArticleCommand.runUseCase(): Kind> { val cmd = this return MD.binding { getArticleBySlug(cmd.slug, user.some()).bind().fold( @@ -109,8 +111,9 @@ interface DeleteArticleUseCase { interface GetArticlesUseCase { val getArticles: GetArticles val getArticlesCount: GetArticlesCount + val MD: MonadDefer - fun GetArticlesCommand.runUseCase(MD: MonadDefer): Kind, Long>> { + fun GetArticlesCommand.runUseCase(): Kind, Long>> { val cmd = this return MD.binding { val count = getArticlesCount(cmd.filter).bind() @@ -127,8 +130,9 @@ interface GetArticlesUseCase { interface GetFeedsUseCase { val getFeeds: GetFeeds val getFeedsCount: GetFeedsCount + val MD: MonadDefer - fun GetFeedsCommand.runUseCase(MD: MonadDefer): Kind, Long>> { + fun GetFeedsCommand.runUseCase(): Kind, Long>> { val cmd = this return MD.binding { val count = getFeedsCount(cmd.user).bind() @@ -144,8 +148,9 @@ interface GetFeedsUseCase { interface UpdateArticleUseCase { val validateUpdate: ValidateArticleUpdate val updateArticle: UpdateArticle + val MD: MonadDefer - fun UpdateArticleCommand.runUseCase(MD: MonadDefer): Kind> { + fun UpdateArticleCommand.runUseCase(): Kind> { val cmd = this return EitherT.monad(MD).binding { val validUpdate = EitherT(validateUpdate(cmd.data, cmd.slug, cmd.user)).bind() @@ -159,8 +164,9 @@ interface UpdateArticleUseCase { interface FavoriteUseCase { val getArticleBySlug: GetArticleBySlug val addFavorite: AddFavorite + val MD: MonadDefer - fun FavoriteArticleCommand.runUseCase(MD: MonadDefer): Kind> { + fun FavoriteArticleCommand.runUseCase(): Kind> { val cmd = this return MD.binding { getArticleBySlug(cmd.slug, cmd.user.some()).bind().fold( @@ -185,8 +191,9 @@ interface FavoriteUseCase { interface UnfavoriteUseCase { val getArticleBySlug: GetArticleBySlug val removeFavorite: RemoveFavorite + val MD: MonadDefer - fun UnfavoriteArticleCommand.runUseCase(MD: MonadDefer): Kind> { + fun UnfavoriteArticleCommand.runUseCase(): Kind> { val cmd = this return MD.binding { getArticleBySlug(cmd.slug, cmd.user.some()).bind().fold( @@ -209,8 +216,9 @@ interface UnfavoriteUseCase { interface CommentUseCase { val getArticleBySlug: GetArticleBySlug val addComment: AddComment + val MD: MonadDefer - fun CommentArticleCommand.runUsecase(MD: MonadDefer): Kind> { + fun CommentArticleCommand.runUsecase(): Kind> { val cmd = this return MD.binding { getArticleBySlug(cmd.slug, cmd.user.some()).bind().fold( @@ -225,8 +233,9 @@ interface DeleteCommentUseCase { val getArticleBySlug: GetArticleBySlug val getComment: GetComment val deleteComment: DeleteComment + val MD: MonadDefer - fun DeleteCommentCommand.runUseCase(MD: MonadDefer): Kind> { + fun DeleteCommentCommand.runUseCase(): Kind> { val cmd = this return MD.binding { getArticleBySlug(cmd.slug, cmd.user.some()).bind().fold( @@ -251,8 +260,9 @@ interface DeleteCommentUseCase { interface GetCommentsUseCase { val getArticleBySlug: GetArticleBySlug val getComments: GetComments + val MD: MonadDefer - fun GetCommentsCommand.runUseCase(MD: MonadDefer): Kind>> { + fun GetCommentsCommand.runUseCase(): Kind>> { val cmd = this return MD.binding { getArticleBySlug(cmd.slug, cmd.user).bind().fold( diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/UseCases.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/UseCases.kt index 5990b11..68f502a 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/UseCases.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/UseCases.kt @@ -16,8 +16,9 @@ data class UnfollowCommand(val username: String, val current: User) interface GetProfileUseCase { val getUser: GetUserByUsername val hasFollower: HasFollower + val MD: MonadDefer - fun GetProfileCommand.runUseCase(MD: MonadDefer): Kind> { + fun GetProfileCommand.runUseCase(): Kind> { val cmd = this return MD.binding { getUser(cmd.username).bind().fold( @@ -41,8 +42,9 @@ interface GetProfileUseCase { interface FollowUseCase { val getUser: GetUserByUsername val addFollower: AddFollower + val MD: MonadDefer - fun FollowCommand.runUseCase(MD: MonadDefer): Kind> { + fun FollowCommand.runUseCase(): Kind> { val cmd = this return MD.binding { getUser(cmd.username).bind().fold( @@ -64,8 +66,9 @@ interface FollowUseCase { interface UnfollowUseCase { val getUser: GetUserByUsername val removeFollower: RemoveFollower + val MD: MonadDefer - fun UnfollowCommand.runUseCase(MD: MonadDefer): Kind> { + fun UnfollowCommand.runUseCase(): Kind> { val cmd = this return MD.binding { getUser(cmd.username).bind().fold( diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt index d254234..40893db 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt @@ -32,8 +32,9 @@ interface RegisterUserUseCase { val auth: Auth val createUser: CreateUser val validateUser: ValidateUserRegistration + val MD: MonadDefer - fun RegisterUserCommand.runUseCase(MD: MonadDefer): Kind> { + fun RegisterUserCommand.runUseCase(): Kind> { val cmd = this return EitherT.monad(MD).binding { val validRegistration = EitherT(validateUser(cmd.data)).bind() @@ -47,8 +48,9 @@ interface RegisterUserUseCase { interface LoginUserUseCase { val auth: Auth val getUser: GetUserByEmail + val MD: MonadDefer - fun LoginUserCommand.runUseCase(MD: MonadDefer): Kind> { + fun LoginUserCommand.runUseCase(): Kind> { val cmd = this return EitherT.monad(MD).binding { val userAndPassword = EitherT( @@ -71,8 +73,9 @@ interface UpdateUserUseCase { val auth: Auth val validateUpdate: ValidateUserUpdate val updateUser: UpdateUser + val MD: MonadDefer - fun UpdateUserCommand.runUseCase(MD: MonadDefer): Kind> { + fun UpdateUserCommand.runUseCase(): Kind> { val cmd = this return EitherT.monad(MD).binding { val validUpdate = EitherT(validateUpdate(cmd.data, cmd.current)).bind() diff --git a/realworld-domain/src/test/kotlin/io/realworld/domain/users/UsersTest.kt b/realworld-domain/src/test/kotlin/io/realworld/domain/users/UsersTest.kt index fb902a1..2b0b0b9 100644 --- a/realworld-domain/src/test/kotlin/io/realworld/domain/users/UsersTest.kt +++ b/realworld-domain/src/test/kotlin/io/realworld/domain/users/UsersTest.kt @@ -35,6 +35,7 @@ class RegisterUserWorkflowTests { override val auth = auth0 override val createUser = createUser0 override val validateUser = { x: UserRegistration -> x.autovalid().right().liftIO() } + override val MD = IO.monadDefer() }.test(userRegistration).unsafeRunSync() assertThat(actual.isRight()).isTrue() @@ -45,8 +46,9 @@ class RegisterUserWorkflowTests { assertThatThrownBy { object : RegisterUserUseCase { override val auth = auth0 - override val createUser: CreateUser = { _ -> IO.raiseError(RuntimeException("BOOM!")) } + override val createUser: CreateUser = { IO.raiseError(RuntimeException("BOOM!")) } override val validateUser = { x: UserRegistration -> x.autovalid().right().liftIO() } + override val MD = IO.monadDefer() }.test(userRegistration).unsafeRunSync() }.hasMessage("BOOM!") @@ -54,7 +56,8 @@ class RegisterUserWorkflowTests { object : RegisterUserUseCase { override val auth = auth0 override val createUser = createUser0 - override val validateUser: ValidateUserRegistration = { _ -> IO.raiseError(RuntimeException("BOOM!")) } + override val validateUser: ValidateUserRegistration = { IO.raiseError(RuntimeException("BOOM!")) } + override val MD = IO.monadDefer() }.test(userRegistration).unsafeRunSync() }.hasMessage("BOOM!") } @@ -72,14 +75,15 @@ class RegisterUserWorkflowTests { User(id = UUID.randomUUID().userId(), email = x.email, token = x.token, username = x.username) } } - override val validateUser = { _: UserRegistration -> IO { throw RuntimeException("BOOM!") } } + override val validateUser: ValidateUserRegistration = { IO.raiseError(RuntimeException("BOOM!")) } + override val MD = IO.monadDefer() }.test(userRegistration).unsafeRunSync() } assertThat(userSaved).isFalse() } private fun RegisterUserUseCase.test(input: UserRegistration) = this.run { - RegisterUserCommand(input).runUseCase(IO.monadDefer()).fix() + RegisterUserCommand(input).runUseCase().fix() } private fun UserRegistration.autovalid() = UUID.randomUUID().userId().let { From 9136933de014538ad8f6bdcb9cf61ba3d986197d Mon Sep 17 00:00:00 2001 From: Isto Nikula Date: Sun, 13 Jan 2019 12:12:07 +0200 Subject: [PATCH 06/10] refactor(tx): apply idiomatic kotlin --- realworld-app/web/src/main/kotlin/io/realworld/Tx.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/realworld-app/web/src/main/kotlin/io/realworld/Tx.kt b/realworld-app/web/src/main/kotlin/io/realworld/Tx.kt index a62e87c..80ffc31 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/Tx.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/Tx.kt @@ -8,14 +8,14 @@ import org.springframework.transaction.support.TransactionTemplate fun IO.runReadTx(txManager: PlatformTransactionManager): A = TransactionTemplate(txManager) .apply { isReadOnly = true - setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED) + isolationLevel = TransactionDefinition.ISOLATION_READ_COMMITTED propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRES_NEW } - .execute({ this.unsafeRunSync() })!! + .execute { this.unsafeRunSync() }!! fun IO.runWriteTx(txManager: PlatformTransactionManager): A = TransactionTemplate(txManager) .apply { - setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED) + isolationLevel = TransactionDefinition.ISOLATION_READ_COMMITTED propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRES_NEW } - .execute({ this.unsafeRunSync() })!! + .execute { this.unsafeRunSync() }!! From 1ccac484248f1961b55b5cad6aafdb37ffabe552 Mon Sep 17 00:00:00 2001 From: Isto Nikula Date: Sun, 13 Jan 2019 12:13:55 +0200 Subject: [PATCH 07/10] refactor(repos): postfix impure public methods having pure counterparts with 'Impure' --- .../kotlin/io/realworld/persistence/ArticleRepository.kt | 4 ++-- .../kotlin/io/realworld/persistence/UserRepository.kt | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt index cdc44c7..d1bd9f5 100644 --- a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt +++ b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/ArticleRepository.kt @@ -377,13 +377,13 @@ class ArticleRepository( } private fun fetchAuthor(id: UserId, querier: Option): Profile = - userRepo.findByIdEff(id).map { + userRepo.findByIdImpure(id).map { it.user.let { author -> Profile( username = author.username, bio = author.bio.toOption(), image = author.image.toOption(), - following = querier.map { userRepo.hasFollowerEff(author.id, it.id) } + following = querier.map { userRepo.hasFollowerImpure(author.id, it.id) } ) } }.getOrElse { throw RuntimeException("Corrupt DB: article author $id not found") } diff --git a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt index 1d1c79c..be682c9 100644 --- a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt +++ b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt @@ -87,9 +87,9 @@ open class UserRepository( } fun findById(id: UserId): Kind> = - defer { findByIdEff(id).just() } + defer { findByIdImpure(id).just() } - fun findByIdEff(id: UserId): Option = + fun findByIdImpure(id: UserId): Option = DataAccessUtils.singleResult( jdbcTemplate.query( "SELECT * FROM ${UserTbl.table} WHERE ${UserTbl.id.eq()}", @@ -132,10 +132,10 @@ open class UserRepository( } fun hasFollower(followee: UserId, follower: UserId): Kind = defer { - hasFollowerEff(followee, follower).just() + hasFollowerImpure(followee, follower).just() } - fun hasFollowerEff(followee: UserId, follower: UserId): Boolean = FollowTbl.let { + fun hasFollowerImpure(followee: UserId, follower: UserId): Boolean = FollowTbl.let { jdbcTemplate.queryIfExists( it.table, "${it.followee.eq()} AND ${it.follower.eq()}", From 68c345d4bfc6d63f37463e891b435cd207237c7c Mon Sep 17 00:00:00 2001 From: Isto Nikula Date: Sun, 13 Jan 2019 12:18:36 +0200 Subject: [PATCH 08/10] refactor(profiles): it's enough to require Monad instance in domain --- .../main/kotlin/io/realworld/profiles/Routes.kt | 9 +++------ .../io/realworld/domain/profiles/UseCases.kt | 14 +++++++------- .../io/realworld/persistence/UserRepository.kt | 2 +- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt b/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt index 53350b7..131505f 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt @@ -1,9 +1,7 @@ package io.realworld.profiles import arrow.effects.ForIO -import arrow.effects.IO import arrow.effects.fix -import arrow.effects.instances.io.monadDefer.monadDefer import io.realworld.JwtTokenResolver import io.realworld.authHeader import io.realworld.domain.common.Auth @@ -38,7 +36,6 @@ class ProfileController( private val auth: Auth, private val repo: UserRepository, private val txManager: PlatformTransactionManager - ) { @GetMapping("/api/profiles/{username}") fun getProfile( @@ -55,7 +52,7 @@ class ProfileController( return object : GetProfileUseCase { override val getUser = repo::findByUsername override val hasFollower = repo::hasFollower - override val MD = IO.monadDefer() + override val M = repo.MD }.run { GetProfileCommand(username, user).runUseCase() }.fix().runReadTx(txManager).fold( @@ -72,7 +69,7 @@ class ProfileController( return object : FollowUseCase { override val addFollower = repo::addFollower override val getUser = repo::findByUsername - override val MD = IO.monadDefer() + override val M = repo.MD }.run { FollowCommand(username, current).runUseCase() }.fix().runWriteTx(txManager).fold( @@ -89,7 +86,7 @@ class ProfileController( return object : UnfollowUseCase { override val getUser = repo::findByUsername override val removeFollower = repo::removeFollower - override val MD = IO.monadDefer() + override val M = repo.MD }.run { UnfollowCommand(username, current).runUseCase() }.fix().runWriteTx(txManager).fold( diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/UseCases.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/UseCases.kt index 68f502a..14ed744 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/UseCases.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/profiles/UseCases.kt @@ -5,7 +5,7 @@ import arrow.core.Option import arrow.core.none import arrow.core.some import arrow.core.toOption -import arrow.effects.typeclasses.MonadDefer +import arrow.typeclasses.Monad import arrow.typeclasses.binding import io.realworld.domain.users.User @@ -16,11 +16,11 @@ data class UnfollowCommand(val username: String, val current: User) interface GetProfileUseCase { val getUser: GetUserByUsername val hasFollower: HasFollower - val MD: MonadDefer + val M: Monad fun GetProfileCommand.runUseCase(): Kind> { val cmd = this - return MD.binding { + return M.binding { getUser(cmd.username).bind().fold( { none() }, { @@ -42,11 +42,11 @@ interface GetProfileUseCase { interface FollowUseCase { val getUser: GetUserByUsername val addFollower: AddFollower - val MD: MonadDefer + val M: Monad fun FollowCommand.runUseCase(): Kind> { val cmd = this - return MD.binding { + return M.binding { getUser(cmd.username).bind().fold( { none() }, { @@ -66,11 +66,11 @@ interface FollowUseCase { interface UnfollowUseCase { val getUser: GetUserByUsername val removeFollower: RemoveFollower - val MD: MonadDefer + val M: Monad fun UnfollowCommand.runUseCase(): Kind> { val cmd = this - return MD.binding { + return M.binding { getUser(cmd.username).bind().fold( { none() }, { diff --git a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt index be682c9..6f6f688 100644 --- a/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt +++ b/realworld-infra/persistence/src/main/kotlin/io/realworld/persistence/UserRepository.kt @@ -20,7 +20,7 @@ import java.util.UUID open class UserRepository( private val jdbcTemplate: NamedParameterJdbcTemplate, - MD: MonadDefer + val MD: MonadDefer ) : MonadDefer by MD { private fun User.Companion.fromRs(rs: ResultSet) = with(UserTbl) { From 560d423bc399a891a3b303623e21e96fbf68459d Mon Sep 17 00:00:00 2001 From: Isto Nikula Date: Sun, 13 Jan 2019 12:22:32 +0200 Subject: [PATCH 09/10] refactor(users): it's enough to require Monad instance in domain --- .../main/kotlin/io/realworld/users/Routes.kt | 14 +++++------- .../io/realworld/domain/users/Services.kt | 10 ++++----- .../io/realworld/domain/users/UseCases.kt | 22 +++++++++---------- .../io/realworld/domain/users/UsersTest.kt | 10 ++++----- 4 files changed, 26 insertions(+), 30 deletions(-) diff --git a/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt b/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt index 45bb097..8b6ce11 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/users/Routes.kt @@ -2,9 +2,7 @@ package io.realworld.users import arrow.core.Option import arrow.effects.ForIO -import arrow.effects.IO import arrow.effects.fix -import arrow.effects.instances.io.monadDefer.monadDefer import io.realworld.FieldError import io.realworld.UnauthorizedException import io.realworld.domain.common.Auth @@ -56,19 +54,18 @@ class UserController( @PostMapping("/api/users") fun register(@Valid @RequestBody registration: RegistrationDto): ResponseEntity { - val ioMonadDefer = IO.monadDefer() val validateUserSrv = object : ValidateUserService { override val auth = auth0 override val existsByEmail = repo::existsByEmail override val existsByUsername = repo::existsByUsername - override val MD = ioMonadDefer + override val M = repo.MD } return object : RegisterUserUseCase { override val auth = auth0 override val createUser: CreateUser = repo::create override val validateUser: ValidateUserRegistration = { x -> validateUserSrv.run { x.validate() } } - override val MD = ioMonadDefer + override val M = repo.MD }.run { RegisterUserCommand(UserRegistration( username = registration.username, @@ -93,7 +90,7 @@ class UserController( return object : LoginUserUseCase { override val auth = auth0 override val getUser: GetUserByEmail = repo::findByEmail - override val MD = IO.monadDefer() + override val M = repo.MD }.run { LoginUserCommand( email = login.email, @@ -107,19 +104,18 @@ class UserController( @PutMapping("/api/user") fun update(@Valid @RequestBody update: UserUpdateDto, user: User): ResponseEntity { - val ioMonadDefer = IO.monadDefer() val validateUpdateSrv = object : ValidateUserUpdateService { override val auth = auth0 override val existsByEmail = repo::existsByEmail override val existsByUsername = repo::existsByUsername - override val MD = ioMonadDefer + override val M = repo.MD } return object : UpdateUserUseCase { override val auth = auth0 override val validateUpdate: ValidateUserUpdate = { x, y -> validateUpdateSrv.run { x.validate(y) } } override val updateUser: UpdateUser = repo::update - override val MD = ioMonadDefer + override val M = repo.MD }.run { UpdateUserCommand( data = UserUpdate( diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/users/Services.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/users/Services.kt index 3244003..28aa351 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/users/Services.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/users/Services.kt @@ -5,7 +5,7 @@ import arrow.core.Either import arrow.core.getOrElse import arrow.core.left import arrow.core.right -import arrow.effects.typeclasses.MonadDefer +import arrow.typeclasses.Monad import arrow.typeclasses.binding import io.realworld.domain.common.Auth import io.realworld.domain.common.Token @@ -15,11 +15,11 @@ interface ValidateUserService { val auth: Auth val existsByUsername: ExistsByUsername val existsByEmail: ExistsByEmail - val MD: MonadDefer + val M: Monad fun UserRegistration.validate(): Kind> { val cmd = this - return MD.binding { + return M.binding { when { existsByEmail(cmd.email).bind() -> UserRegistrationError.EmailAlreadyTaken.left() @@ -44,11 +44,11 @@ interface ValidateUserUpdateService { val auth: Auth val existsByUsername: ExistsByUsername val existsByEmail: ExistsByEmail - val MD: MonadDefer + val M: Monad fun UserUpdate.validate(current: User): Kind> { val cmd = this - return MD.binding { + return M.binding { when { cmd.email.fold({ false }, { current.email !== it && existsByEmail(it).bind() }) -> UserUpdateError.EmailAlreadyTaken.left() diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt index 40893db..cab8205 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/users/UseCases.kt @@ -6,8 +6,8 @@ import arrow.core.left import arrow.core.right import arrow.data.EitherT import arrow.data.value -import arrow.effects.typeclasses.MonadDefer import arrow.instances.monad +import arrow.typeclasses.Monad import arrow.typeclasses.binding import io.realworld.domain.common.Auth @@ -32,14 +32,14 @@ interface RegisterUserUseCase { val auth: Auth val createUser: CreateUser val validateUser: ValidateUserRegistration - val MD: MonadDefer + val M: Monad fun RegisterUserCommand.runUseCase(): Kind> { val cmd = this - return EitherT.monad(MD).binding { + return EitherT.monad(M).binding { val validRegistration = EitherT(validateUser(cmd.data)).bind() EitherT( - MD.run { createUser(validRegistration).map { it.right() } } + M.run { createUser(validRegistration).map { it.right() } } ).bind() }.value() } @@ -48,17 +48,17 @@ interface RegisterUserUseCase { interface LoginUserUseCase { val auth: Auth val getUser: GetUserByEmail - val MD: MonadDefer + val M: Monad fun LoginUserCommand.runUseCase(): Kind> { val cmd = this - return EitherT.monad(MD).binding { + return EitherT.monad(M).binding { val userAndPassword = EitherT( - MD.run { + M.run { getUser(cmd.email).map { it.toEither { UserLoginError.BadCredentials } } } ).bind() - EitherT(MD.just( + EitherT(M.just( when (auth.checkPassword(cmd.password, userAndPassword.encryptedPassword)) { true -> userAndPassword.right() false -> UserLoginError.BadCredentials.left() @@ -73,14 +73,14 @@ interface UpdateUserUseCase { val auth: Auth val validateUpdate: ValidateUserUpdate val updateUser: UpdateUser - val MD: MonadDefer + val M: Monad fun UpdateUserCommand.runUseCase(): Kind> { val cmd = this - return EitherT.monad(MD).binding { + return EitherT.monad(M).binding { val validUpdate = EitherT(validateUpdate(cmd.data, cmd.current)).bind() EitherT( - MD.run { updateUser(validUpdate, current).map { it.right() } } + M.run { updateUser(validUpdate, current).map { it.right() } } ).bind() }.value() } diff --git a/realworld-domain/src/test/kotlin/io/realworld/domain/users/UsersTest.kt b/realworld-domain/src/test/kotlin/io/realworld/domain/users/UsersTest.kt index 2b0b0b9..0e7a155 100644 --- a/realworld-domain/src/test/kotlin/io/realworld/domain/users/UsersTest.kt +++ b/realworld-domain/src/test/kotlin/io/realworld/domain/users/UsersTest.kt @@ -5,7 +5,7 @@ import arrow.core.right import arrow.effects.ForIO import arrow.effects.IO import arrow.effects.fix -import arrow.effects.instances.io.monadDefer.monadDefer +import arrow.effects.instances.io.monad.monad import arrow.effects.liftIO import io.realworld.domain.common.Auth import io.realworld.domain.common.Settings @@ -35,7 +35,7 @@ class RegisterUserWorkflowTests { override val auth = auth0 override val createUser = createUser0 override val validateUser = { x: UserRegistration -> x.autovalid().right().liftIO() } - override val MD = IO.monadDefer() + override val M = IO.monad() }.test(userRegistration).unsafeRunSync() assertThat(actual.isRight()).isTrue() @@ -48,7 +48,7 @@ class RegisterUserWorkflowTests { override val auth = auth0 override val createUser: CreateUser = { IO.raiseError(RuntimeException("BOOM!")) } override val validateUser = { x: UserRegistration -> x.autovalid().right().liftIO() } - override val MD = IO.monadDefer() + override val M = IO.monad() }.test(userRegistration).unsafeRunSync() }.hasMessage("BOOM!") @@ -57,7 +57,7 @@ class RegisterUserWorkflowTests { override val auth = auth0 override val createUser = createUser0 override val validateUser: ValidateUserRegistration = { IO.raiseError(RuntimeException("BOOM!")) } - override val MD = IO.monadDefer() + override val M = IO.monad() }.test(userRegistration).unsafeRunSync() }.hasMessage("BOOM!") } @@ -76,7 +76,7 @@ class RegisterUserWorkflowTests { } } override val validateUser: ValidateUserRegistration = { IO.raiseError(RuntimeException("BOOM!")) } - override val MD = IO.monadDefer() + override val M = IO.monad() }.test(userRegistration).unsafeRunSync() } assertThat(userSaved).isFalse() From ad045068743ede93eafe024c500f639fb9e1677d Mon Sep 17 00:00:00 2001 From: Isto Nikula Date: Sun, 13 Jan 2019 13:40:01 +0200 Subject: [PATCH 10/10] chore(delete-article): experiment using MonadError instead of explicit Either --- .../main/kotlin/io/realworld/articles/Routes.kt | 11 +++++++---- .../io/realworld/domain/articles/UseCases.kt | 17 +++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt b/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt index c9525a5..0745a35 100644 --- a/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt +++ b/realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt @@ -195,14 +195,17 @@ class ArticleController( return object : DeleteArticleUseCase { override val getArticleBySlug = articleRepo::getBySlug override val deleteArticle = articleRepo::deleteArticle - override val MD = IO.monadDefer() + override val ME = IO.monadDefer() }.run { DeleteArticleCommand(slug, user).runUseCase() - }.fix().runWriteTx(txManager).fold( + }.fix().attempt().runWriteTx(txManager).fold( { when (it) { - is ArticleDeleteError.NotAuthor -> throw ForbiddenException() - is ArticleDeleteError.NotFound -> ResponseEntity.notFound().build() + is ArticleDeleteError -> when (it) { + is ArticleDeleteError.NotAuthor -> throw ForbiddenException() + is ArticleDeleteError.NotFound -> ResponseEntity.notFound().build() + } + else -> throw it } }, { ResponseEntity.noContent().build() } diff --git a/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt b/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt index 26baf0f..77c9bed 100644 --- a/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt +++ b/realworld-domain/src/main/kotlin/io/realworld/domain/articles/UseCases.kt @@ -12,6 +12,7 @@ import arrow.data.EitherT import arrow.data.value import arrow.effects.typeclasses.MonadDefer import arrow.instances.monad +import arrow.typeclasses.MonadError import arrow.typeclasses.binding import io.realworld.domain.users.User import java.util.UUID @@ -34,7 +35,7 @@ sealed class ArticleUpdateError { object NotFound : ArticleUpdateError() } -sealed class ArticleDeleteError { +sealed class ArticleDeleteError : RuntimeException() { object NotAuthor : ArticleDeleteError() object NotFound : ArticleDeleteError() } @@ -92,18 +93,18 @@ interface GetArticleUseCase { interface DeleteArticleUseCase { val getArticleBySlug: GetArticleBySlug val deleteArticle: DeleteArticle - val MD: MonadDefer + val ME: MonadError - fun DeleteArticleCommand.runUseCase(): Kind> { + fun DeleteArticleCommand.runUseCase(): Kind { val cmd = this - return MD.binding { + return ME.binding { getArticleBySlug(cmd.slug, user.some()).bind().fold( - { ArticleDeleteError.NotFound.left() }, + { ME.raiseError(ArticleDeleteError.NotFound) }, { - if (it.author.username != cmd.user.username) ArticleDeleteError.NotAuthor.left() - else deleteArticle(it.id).bind().right() + if (it.author.username != cmd.user.username) ME.raiseError(ArticleDeleteError.NotAuthor) + else deleteArticle(it.id) } - ) + ).bind() } } }