diff --git a/build.gradle b/build.gradle index b63f091..069f41f 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,7 @@ version = '0.0.1-SNAPSHOT' subprojects { apply plugin: 'kotlin' apply plugin: 'org.junit.platform.gradle.plugin' + apply plugin: "org.jetbrains.kotlin.plugin.allopen" junitPlatform { filters { @@ -35,9 +36,18 @@ subprojects { dependencies { compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8") compile("org.jetbrains.kotlin:kotlin-reflect") - testCompile('org.jetbrains.spek:spek-api:1.1.5') { - exclude group: 'org.jetbrains.kotlin' - } + compile 'io.arrow-kt:arrow-core:0.7.2' + compile 'io.arrow-kt:arrow-syntax:0.7.2' + compile 'io.arrow-kt:arrow-typeclasses:0.7.2' + compile 'io.arrow-kt:arrow-data:0.7.2' + compile 'io.arrow-kt:arrow-instances-core:0.7.2' + compile 'io.arrow-kt:arrow-instances-data:0.7.2' + kapt 'io.arrow-kt:arrow-annotations-processor:0.7.2' + + testCompile('org.jetbrains.spek:spek-api:1.1.5') + { + exclude group: 'org.jetbrains.kotlin' + } testRuntime('org.jetbrains.spek:spek-junit-platform-engine:1.1.5') { exclude group: 'org.junit.platform' exclude group: 'org.jetbrains.kotlin' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 01b8bf6..1948b90 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 933b647..d2c45a4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zip diff --git a/business-rules/build.gradle b/interactors/build.gradle similarity index 100% rename from business-rules/build.gradle rename to interactors/build.gradle diff --git a/interactors/src/main/kotlin/entities/Exam.kt b/interactors/src/main/kotlin/entities/Exam.kt new file mode 100644 index 0000000..196ce30 --- /dev/null +++ b/interactors/src/main/kotlin/entities/Exam.kt @@ -0,0 +1,14 @@ +package com.students.results.entities + +import arrow.core.Either +import services.Grade + +data class Exam(val id: Long) { + fun validateGrade(notation: Grade): Either = + when (notation) { + in (0.toBigDecimal()..20.toBigDecimal()) -> Either.right(Unit) + else -> Either.left(InvalidNotationForThisExamException("This notation, $notation, is not in bound")) + } +} + +class InvalidNotationForThisExamException(msg: String?) : Exception(msg) diff --git a/interactors/src/main/kotlin/entities/Student.kt b/interactors/src/main/kotlin/entities/Student.kt new file mode 100644 index 0000000..1ebe399 --- /dev/null +++ b/interactors/src/main/kotlin/entities/Student.kt @@ -0,0 +1,22 @@ +package com.students.results.entities + +import arrow.core.Either +import services.Grade +import java.math.BigDecimal + +typealias Grades = Map + +data class Student(val id: Long, val grades: Grades = emptyMap()) { + fun getNotation(exam: Exam): Either = + when { + grades.containsKey(exam) -> Either.right(grades.getValue(exam)) + else -> Either.left(NotEvaluatedException()) + } + + fun notate(exam: Exam, note: BigDecimal): Either = + exam.validateGrade(note).map { + copy(grades = grades + mapOf(exam to note)) + } +} + +class NotEvaluatedException(msg: String? = null, throwable: Throwable? = null) : Exception(msg, throwable) \ No newline at end of file diff --git a/interactors/src/main/kotlin/interactors/ExamsInteractor.kt b/interactors/src/main/kotlin/interactors/ExamsInteractor.kt new file mode 100644 index 0000000..575e80d --- /dev/null +++ b/interactors/src/main/kotlin/interactors/ExamsInteractor.kt @@ -0,0 +1,6 @@ +package com.students.results.interactors + +import com.students.results.repository.ExamsRepository +import com.students.results.services.Exams + +class ExamsInteractor(private val examsRepository: ExamsRepository) : Exams diff --git a/interactors/src/main/kotlin/interactors/StudentsInteractor.kt b/interactors/src/main/kotlin/interactors/StudentsInteractor.kt new file mode 100644 index 0000000..107b4ed --- /dev/null +++ b/interactors/src/main/kotlin/interactors/StudentsInteractor.kt @@ -0,0 +1,21 @@ +package com.students.results.interactors + +import arrow.core.Either +import arrow.core.flatMap +import com.students.results.repository.ExamsRepository +import com.students.results.repository.StudentsRepository +import com.students.results.services.GradeExamException +import com.students.results.services.Students +import com.students.results.services.requests.GradeExam + +class StudentsInteractor(private val studentsRepository: StudentsRepository, private val examsRepository: ExamsRepository) : Students { + + override fun grade(gradeExam: GradeExam): Either = + gradeExam.run { + studentsRepository.findStudentById(studentId).flatMap { student -> + examsRepository.findExamById(examId).flatMap { exam -> + student.notate(exam, grade).flatMap { studentsRepository.save(it) } + } + }.mapLeft { GradeExamException(throwable = it) } + } +} diff --git a/interactors/src/main/kotlin/repository/ExamsRepository.kt b/interactors/src/main/kotlin/repository/ExamsRepository.kt new file mode 100644 index 0000000..774d5d9 --- /dev/null +++ b/interactors/src/main/kotlin/repository/ExamsRepository.kt @@ -0,0 +1,9 @@ +package com.students.results.repository + +import arrow.core.Either +import com.students.results.entities.Exam + +interface ExamsRepository { + + fun findExamById(examId: Long): Either +} \ No newline at end of file diff --git a/interactors/src/main/kotlin/repository/Repositories.kt b/interactors/src/main/kotlin/repository/Repositories.kt new file mode 100644 index 0000000..658112a --- /dev/null +++ b/interactors/src/main/kotlin/repository/Repositories.kt @@ -0,0 +1,8 @@ +package com.students.results.repository + + +sealed class RepositoryException(msg: String? = null, throwable: Throwable? = null) : Exception(msg, throwable) + +class NotFoundException(msg: String? = null, throwable: Throwable? = null) : RepositoryException(msg, throwable) +class NotWrittenException(msg: String? = null, throwable: Throwable? = null) : RepositoryException(msg, throwable) +class RepositoryNotAvailableException(msg: String? = null, throwable: Throwable? = null) : RepositoryException(msg, throwable) \ No newline at end of file diff --git a/interactors/src/main/kotlin/repository/StudentsRepository.kt b/interactors/src/main/kotlin/repository/StudentsRepository.kt new file mode 100644 index 0000000..e2f4f4d --- /dev/null +++ b/interactors/src/main/kotlin/repository/StudentsRepository.kt @@ -0,0 +1,10 @@ +package com.students.results.repository + +import arrow.core.Either +import com.students.results.entities.Student + +interface StudentsRepository { + + fun findStudentById(studentId: Long): Either + fun save(student: Student): Either +} \ No newline at end of file diff --git a/interactors/src/main/kotlin/services/Exams.kt b/interactors/src/main/kotlin/services/Exams.kt new file mode 100644 index 0000000..61b6a16 --- /dev/null +++ b/interactors/src/main/kotlin/services/Exams.kt @@ -0,0 +1,6 @@ +package com.students.results.services + +interface Exams { +} + +class GradeExamException(message: String? = null, throwable: Throwable? = null) : Exception(message, throwable) diff --git a/interactors/src/main/kotlin/services/Services.kt b/interactors/src/main/kotlin/services/Services.kt new file mode 100644 index 0000000..10e28e4 --- /dev/null +++ b/interactors/src/main/kotlin/services/Services.kt @@ -0,0 +1,8 @@ +package services + +import java.math.BigDecimal + +typealias Grade = BigDecimal +sealed class ServicesException(msg: String? = null, throwable: Throwable? = null) : Exception(msg, throwable) + +class NotFoundException(msg: String? = null, throwable: Throwable? = null) : ServicesException(msg, throwable) \ No newline at end of file diff --git a/interactors/src/main/kotlin/services/Students.kt b/interactors/src/main/kotlin/services/Students.kt new file mode 100644 index 0000000..b34f748 --- /dev/null +++ b/interactors/src/main/kotlin/services/Students.kt @@ -0,0 +1,10 @@ +package com.students.results.services + +import arrow.core.Either +import com.students.results.services.requests.GradeExam + +interface Students { + + fun grade(gradeExam: GradeExam): Either + +} diff --git a/interactors/src/main/kotlin/services/requests/GradeExam.kt b/interactors/src/main/kotlin/services/requests/GradeExam.kt new file mode 100644 index 0000000..84c1e79 --- /dev/null +++ b/interactors/src/main/kotlin/services/requests/GradeExam.kt @@ -0,0 +1,5 @@ +package com.students.results.services.requests + +import services.Grade + +data class GradeExam(val examId: Long, val studentId: Long, val grade: Grade) diff --git a/interactors/src/test/kotlin/entities/StudentTest.kt b/interactors/src/test/kotlin/entities/StudentTest.kt new file mode 100644 index 0000000..ed8d9d5 --- /dev/null +++ b/interactors/src/test/kotlin/entities/StudentTest.kt @@ -0,0 +1,35 @@ +package entities + +import com.students.results.entities.Exam +import com.students.results.entities.Student +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldEqual +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it + +class StudentTest : Spek({ + + given("a student") { + val student = Student(id = 40L) + val exam = Exam(id = 50L) + + it("getNot without grade should returns NotEvaluatedException") { + student.getNotation(exam).isLeft() shouldBe true + } + it("grade an exam with 20 and get notation should return 20") { + student.notate(exam, "20".toBigDecimal()).apply { + isRight() shouldBe true + map { + it.getNotation(exam).apply { + isRight() shouldBe true + map { it shouldEqual "20".toBigDecimal() } + } + } + } + } + it("grade less than zero should fail to register grade") { + student.notate(exam, "-1".toBigDecimal()).isLeft() shouldBe true + } + } +}) \ No newline at end of file diff --git a/interactors/src/test/kotlin/interactors/ExamsInteractorTest.kt b/interactors/src/test/kotlin/interactors/ExamsInteractorTest.kt new file mode 100644 index 0000000..07b0b2d --- /dev/null +++ b/interactors/src/test/kotlin/interactors/ExamsInteractorTest.kt @@ -0,0 +1,7 @@ +package com.students.results.interactors + +import org.jetbrains.spek.api.Spek + +class ExamsInteractorTest : Spek({ + +}) diff --git a/interactors/src/test/kotlin/interactors/StudentsInteractorTest.kt b/interactors/src/test/kotlin/interactors/StudentsInteractorTest.kt new file mode 100644 index 0000000..22b6c6e --- /dev/null +++ b/interactors/src/test/kotlin/interactors/StudentsInteractorTest.kt @@ -0,0 +1,56 @@ +package com.students.results.interactors + +import arrow.core.Either +import com.students.results.entities.Exam +import com.students.results.entities.Student +import com.students.results.repository.ExamsRepository +import com.students.results.repository.StudentsRepository +import com.students.results.services.requests.GradeExam +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldEqual +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it + + +class StudentsInteractorTest : Spek({ + + val student = Student(id = 10L) + given("a student interactor") { + + val studentSlot = slot() + val studentRepository = mockk() { + every { findStudentById(3) } returns Either.right(student) + every { save(student = capture(studentSlot)) } returns Either.right(Unit) + } + val exam = Exam(id = 5) + val examsRepository = mockk().apply { + every { findExamById(5) } returns Either.right(exam) + } + val studentsInteractor = StudentsInteractor(studentRepository, examsRepository) + studentsInteractor.notate20().apply { + it("should retrieve student by his id") { + verify { studentRepository.findStudentById(3) } + } + it("should retrieve exam by id") { + verify { examsRepository.findExamById(5) } + } + it("should save student to the repository with the expected notation of 20 for this exam") { + studentSlot.captured.getNotation(exam).apply { + isRight() shouldBe true + map { it shouldEqual "20".toBigDecimal() } + } + } + } + } +}) + +private fun StudentsInteractor.notate20() = grade(GradeExam( + examId = 5, + studentId = 3, + grade = "20".toBigDecimal() +)) \ No newline at end of file diff --git a/persistence/build.gradle b/persistence/build.gradle index b17bc95..c2a90cf 100644 --- a/persistence/build.gradle +++ b/persistence/build.gradle @@ -1,7 +1,21 @@ - apply plugin: 'kotlin-spring' dependencies { - compile project(':business-rules') + compile project(':interactors') compile group: 'org.springframework.data', name: 'spring-data-redis', version: '2.0.7.RELEASE' + compile group: 'org.apache.commons', name: 'commons-pool2', version: '2.5.0' + compile group: 'redis.clients', name: 'jedis', version: '2.9.0' + + testCompile 'it.ozimov:embedded-redis:0.7.2' + testCompile group: 'org.springframework', name: 'spring-test', version: '5.0.6.RELEASE' + testImplementation( + 'org.junit.jupiter:junit-jupiter-api:5.1.0' + ) + testRuntimeOnly( + 'org.junit.jupiter:junit-jupiter-engine:5.1.0' + ) +} + +test { + useJUnitPlatform() } \ No newline at end of file diff --git a/persistence/src/main/kotlin/redis/configuration/RedisConfiguration.kt b/persistence/src/main/kotlin/redis/configuration/RedisConfiguration.kt new file mode 100644 index 0000000..d111688 --- /dev/null +++ b/persistence/src/main/kotlin/redis/configuration/RedisConfiguration.kt @@ -0,0 +1,20 @@ +package com.students.results.redis.configuration + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories + +@Configuration +@EnableRedisRepositories +class RedisConfiguration { + + @Bean + fun connectionFactory() = JedisConnectionFactory() + + @Bean + fun redisTemplate(jedisConnectionFactory: JedisConnectionFactory) = RedisTemplate().apply { + connectionFactory = jedisConnectionFactory + } +} \ No newline at end of file diff --git a/persistence/src/main/kotlin/repository/impl/ExamsRepositoryImpl.kt b/persistence/src/main/kotlin/repository/impl/ExamsRepositoryImpl.kt new file mode 100644 index 0000000..130a036 --- /dev/null +++ b/persistence/src/main/kotlin/repository/impl/ExamsRepositoryImpl.kt @@ -0,0 +1,12 @@ +package repository.impl + +import arrow.core.Either +import com.students.results.entities.Exam +import com.students.results.repository.ExamsRepository +import com.students.results.repository.NotFoundException + +class ExamsRepositoryImpl : ExamsRepository { + override fun findExamById(examId: Long): Either { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/persistence/src/main/kotlin/repository/impl/StudentsRepositoryImpl.kt b/persistence/src/main/kotlin/repository/impl/StudentsRepositoryImpl.kt new file mode 100644 index 0000000..357d2cb --- /dev/null +++ b/persistence/src/main/kotlin/repository/impl/StudentsRepositoryImpl.kt @@ -0,0 +1,17 @@ +package repository.impl + +import arrow.core.Either +import com.students.results.entities.Student +import com.students.results.repository.RepositoryException +import com.students.results.repository.StudentsRepository + +class StudentsRepositoryImpl : StudentsRepository { + + override fun findStudentById(studentId: Long): Either { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun save(student: Student): Either { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/persistence/src/test/kotlin/embeded/redis/startup/EmbededRedisTest.kt b/persistence/src/test/kotlin/embeded/redis/startup/EmbededRedisTest.kt new file mode 100644 index 0000000..169b2b5 --- /dev/null +++ b/persistence/src/test/kotlin/embeded/redis/startup/EmbededRedisTest.kt @@ -0,0 +1,43 @@ +package embeded.redis.startup + +import com.students.results.redis.configuration.RedisConfiguration +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.* +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.runner.RunWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.junit.jupiter.SpringExtension +import org.springframework.test.context.junit4.SpringRunner +import redis.embedded.RedisServer + +@ExtendWith(SpringExtension::class) +@ContextConfiguration(classes = [RedisConfiguration::class, RedisEmbededConfiguration::class]) +@TestInstance(Lifecycle.PER_CLASS) +abstract class RedisTestConfiguration protected constructor() { + @Autowired + private lateinit var redisServer: RedisServer + + @BeforeAll + fun beforeStart() { + redisServer.start() + } + + @AfterAll + fun afterStart() { + redisServer.stop() + } +} + +class EmbededRedisTest : RedisTestConfiguration() { + + + @Test + fun runATEst() { + println("I am running") + } + +} \ No newline at end of file diff --git a/persistence/src/test/kotlin/embeded/redis/startup/RedisEmbededConfiguration.kt b/persistence/src/test/kotlin/embeded/redis/startup/RedisEmbededConfiguration.kt new file mode 100644 index 0000000..b2ced02 --- /dev/null +++ b/persistence/src/test/kotlin/embeded/redis/startup/RedisEmbededConfiguration.kt @@ -0,0 +1,12 @@ +package embeded.redis.startup + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import redis.embedded.RedisServer + +@Configuration +class RedisEmbededConfiguration { + + @Bean + fun redisServer(): RedisServer = RedisServer(6379) +} \ No newline at end of file diff --git a/persistence/src/test/kotlin/repository/impl/ExamsRepositoryImplTest.kt b/persistence/src/test/kotlin/repository/impl/ExamsRepositoryImplTest.kt new file mode 100644 index 0000000..710a1e9 --- /dev/null +++ b/persistence/src/test/kotlin/repository/impl/ExamsRepositoryImplTest.kt @@ -0,0 +1,14 @@ +package repository.impl + +import embeded.redis.startup.RedisTestConfiguration +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.Assertions.* + +internal class ExamsRepositoryImplTest : RedisTestConfiguration() { + + + @Test + fun findExamById() { + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index fde941e..b28791b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ rootProject.name = 'students-results' -include 'business-rules', 'main', 'persistence' +include 'interactors', 'main', 'persistence'