Commit b7b75f16 authored by Julien Topçu's avatar Julien Topçu

Adding controllers

parent a7cbbf9c
......@@ -40,7 +40,7 @@
<plugins>
<plugin>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.0.0-M2</version>
<version>3.0.0-M1</version>
<executions>
<execution>
<id>dependencies-sanitization</id>
......@@ -93,9 +93,6 @@
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
<jvmTarget>1.8</jvmTarget>
<javaParameters>true</javaParameters>
</configuration>
......
......@@ -39,6 +39,36 @@
</execution>
</executions>
</plugin>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<configuration>
<compilerPlugins>
<plugin>all-open</plugin>
</compilerPlugins>
<pluginOptions>
<option>all-open:annotation=org.craftsrecords.hexarch.DomainService</option>
<option>all-open:annotation=org.craftsrecords.hexarch.Stub</option>
</pluginOptions>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
......
package org.craftsrecords.hexarch
import java.lang.annotation.Inherited
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS)
@Inherited
annotation class Aggregate
package org.craftsrecords.hexarch
import java.lang.annotation.Inherited
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
@Inherited
annotation class DomainService
package org.craftsrecords.hexarch
import java.lang.annotation.Inherited
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS)
@Inherited
annotation class Repository
package org.craftsrecords.hexarch
import java.lang.annotation.Inherited
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
@Inherited
annotation class Stub
package org.craftsrecords.talkadvisor.recommendation
import org.craftsrecords.hexarch.Aggregate
import org.craftsrecords.talkadvisor.recommendation.criteria.Criteria
import org.craftsrecords.talkadvisor.recommendation.talk.Talk
import java.util.*
@Aggregate
class Recommendation(val id: UUID = UUID.randomUUID(), talks: Set<Talk>, val criteria: Criteria) {
val talks = talks.toSet()
......
package org.craftsrecords.talkadvisor.recommendation
import org.craftsrecords.hexarch.DomainService
import org.craftsrecords.talkadvisor.recommendation.api.RecommendTalks
import org.craftsrecords.talkadvisor.recommendation.criteria.Criteria
import org.craftsrecords.talkadvisor.recommendation.criteria.GuestCriteria
import org.craftsrecords.talkadvisor.recommendation.profile.ProfileNotFoundException
import org.craftsrecords.talkadvisor.recommendation.spi.Profiles
import org.craftsrecords.talkadvisor.recommendation.spi.Recommendations
import org.craftsrecords.talkadvisor.recommendation.spi.SearchTalks
import org.craftsrecords.talkadvisor.recommendation.talk.Talk
@DomainService
class TalksAdvisor(private val searchTalks: SearchTalks,
private val recommendations: Recommendations,
private val profiles: Profiles) : RecommendTalks {
override fun to(userId: String): Recommendation {
val profile = profiles.fetch(userId)
val profile = profiles.fetch(userId) ?: throw ProfileNotFoundException(userId)
return recommendTalksSatisfying(profile.preferences)
}
......@@ -24,8 +27,7 @@ class TalksAdvisor(private val searchTalks: SearchTalks,
private fun recommendTalksSatisfying(criteria: Criteria): Recommendation {
val talks = retrieveTalksSatisfying(criteria)
val recommendation = Recommendation(criteria = criteria, talks = talks)
recommendations.save(recommendation)
return recommendation
return recommendations.save(recommendation)
}
private fun retrieveTalksSatisfying(criteria: Criteria): Set<Talk> {
......
......@@ -21,6 +21,4 @@ class Profile(id: String, val preferences: Preferences) {
override fun hashCode(): Int {
return id.hashCode()
}
}
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.profile
class ProfileAlreadyExistsException(userId: String) : RuntimeException("A profile already exists for the user $userId")
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.profile
import org.craftsrecords.hexarch.DomainService
import org.craftsrecords.talkadvisor.recommendation.api.CreateProfile
import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences
import org.craftsrecords.talkadvisor.recommendation.spi.Profiles
class ProfileCreator(val profiles: Profiles) : CreateProfile {
@DomainService
class ProfileCreator(private val profiles: Profiles) : CreateProfile {
override fun forUserWithPreferences(userId: String, preferences: Preferences): Profile {
val profile = Profile(userId, preferences)
profiles.save(profile)
return profile
profiles.fetch(userId)?.let { throw ProfileAlreadyExistsException(userId) }
return profiles.save(profile)
}
}
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.profile
class NoProfileFoundException(val userId: String) : RuntimeException("No profile found to the user $userId")
\ No newline at end of file
class ProfileNotFoundException(val userId: String) : RuntimeException("No profile found for the user $userId")
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.spi
import org.craftsrecords.hexarch.Repository
import org.craftsrecords.talkadvisor.recommendation.profile.Profile
@Repository
interface Profiles {
fun save(profile: Profile)
fun save(profile: Profile): Profile
fun fetch(userId: String): Profile
fun fetch(userId: String): Profile?
}
package org.craftsrecords.talkadvisor.recommendation.spi
import org.craftsrecords.hexarch.Repository
import org.craftsrecords.talkadvisor.recommendation.Recommendation
@Repository
interface Recommendations {
fun save(recommendation: Recommendation)
fun save(recommendation: Recommendation): Recommendation
}
package org.craftsrecords.talkadvisor.recommendation.spi.stubs
import org.craftsrecords.hexarch.Stub
import org.craftsrecords.talkadvisor.recommendation.preferences.Topic
import org.craftsrecords.talkadvisor.recommendation.spi.SearchTalks
import org.craftsrecords.talkadvisor.recommendation.talk.Talk
import java.time.Duration
import kotlin.random.Random
@Stub
class HardCodedTalksSearcher : SearchTalks {
override fun forTopics(topics: Set<Topic>): Set<Talk> {
return createTalks(topics)
......
package org.craftsrecords.talkadvisor.recommendation.spi.stubs
import org.craftsrecords.talkadvisor.recommendation.profile.NoProfileFoundException
import org.craftsrecords.hexarch.Stub
import org.craftsrecords.talkadvisor.recommendation.profile.Profile
import org.craftsrecords.talkadvisor.recommendation.spi.Profiles
@Stub
class InMemoryProfiles(private val profiles: MutableMap<String, Profile> = HashMap()) : Profiles {
override fun fetch(userId: String): Profile {
return profiles[userId] ?: throw NoProfileFoundException(userId)
override fun fetch(userId: String): Profile? {
return profiles[userId]
}
override fun save(profile: Profile) {
override fun save(profile: Profile): Profile {
profiles[profile.id] = profile
return profile
}
}
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.spi.stubs
import org.craftsrecords.hexarch.Stub
import org.craftsrecords.talkadvisor.recommendation.Recommendation
import org.craftsrecords.talkadvisor.recommendation.spi.Recommendations
import java.util.*
@Stub
class InMemoryRecommendations(private val recommendations: MutableMap<UUID, Recommendation> = HashMap()) : Recommendations {
override fun save(recommendation: Recommendation) {
override fun save(recommendation: Recommendation): Recommendation {
recommendations[recommendation.id] = recommendation
return recommendation
}
}
\ No newline at end of file
package org.craftsrecords.talkadvisor
import java.nio.charset.Charset
import kotlin.random.Random
fun Random.nextString() = String(Random.nextBytes(4), Charset.defaultCharset())
\ No newline at end of file
package org.craftsrecords.talkadvisor
package org.craftsrecords.talkadvisor.recommendation
import cucumber.api.CucumberOptions
import cucumber.api.junit.Cucumber
import org.junit.runner.RunWith
@RunWith(Cucumber::class)
@CucumberOptions(strict = true, plugin = ["pretty", "html:target/cucumber"], features = ["classpath:features/"])
class RunCucumberTest
\ No newline at end of file
@CucumberOptions(strict = true, plugin = ["pretty", "html:target/cucumber"], features = ["classpath:features/recommendations.feature"])
class RecommendationFunctionalTests
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation
import org.assertj.core.api.Assertions.assertThat
import org.craftsrecords.talkadvisor.recommendation.profile.createCriteria
import org.craftsrecords.talkadvisor.recommendation.criteria.createCriteria
import org.craftsrecords.talkadvisor.recommendation.talk.createTalk
import org.craftsrecords.talkadvisor.recommendation.talk.createTalks
import org.junit.jupiter.api.Test
......
......@@ -8,12 +8,10 @@ class ProfileAssert(actual: Profile) : AbstractAssert<ProfileAssert, Profile>(
actual,
ProfileAssert::class.java
) {
@JvmName("correspondsToUser")
infix fun `corresponds to user`(userId: String) {
matches({ actual.id == userId }, "profile id corresponds to userId")
}
@JvmName("hasPreferences")
infix fun `has preferences`(preferences: Preferences) {
matches({ actual.preferences == preferences }, "matching expected preferences")
}
......
......@@ -12,12 +12,10 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
RecommendationAssert::class.java
) {
@JvmName("hasTalks")
fun hasTalks() {
matches({ it.talks.isNotEmpty() }, "has talks")
}
@JvmName("hasTalksRelatedTo")
infix fun `has talks related to`(topicName: String) {
matches({
it.criteria.topics.any { topic -> topic.name == topicName }
......@@ -26,7 +24,6 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
actual.talks.those `are related to topic` topicName
}
@JvmName("hasTalksRelatedTo")
infix fun `has talks related to`(topics: Set<Topic>) {
matches({
it.criteria.topics.all { topic -> topics.contains(topic) }
......@@ -35,7 +32,6 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
actual.talks.those `are related to topics` topics
}
@JvmName("hasOnlyTalksInTheFormat")
infix fun `has only talks in the format`(talkFormat: TalkFormat) {
matches({
it.criteria.talksFormats.all { format -> format == talkFormat }
......@@ -44,12 +40,10 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
actual.talks.those `are in the format` talkFormat
}
@JvmName("hasOnlyTalksHavingTheirDurationIn")
infix fun `has only talks having their duration in`(range: ClosedRange<Duration>) {
actual.talks.those `have their duration in ` range
}
@JvmName("hasOnlyTalksInTheFormats")
infix fun `has only talks in the formats`(talksFormats: Set<TalkFormat>) {
matches({
it.criteria.talksFormats.all { talkFormat -> talksFormats.contains(talkFormat) }
......@@ -57,7 +51,6 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
actual.talks.those `have their format in` talksFormats
}
@JvmName("correspondsToTheCriteria")
infix fun `corresponds to the criteria`(criteria: Criteria) {
matches({ it.criteria == criteria }, "corresponds to the given criteria")
}
......
......@@ -16,32 +16,26 @@ class TalksAssert(actual: Iterable<Talk>) : AbstractIterableAssert<TalksAssert,
return TalkAssert(talk).`as`(description)
}
@JvmName("areRelatedToTopic")
infix fun `are related to topic`(topicName: String) {
allSatisfy { TalkAssert(it) `is related to topic` topicName }
}
@JvmName("areRelatedToTopic")
infix fun `are related to topics`(topics: Set<Topic>) {
allSatisfy { TalkAssert(it) `is related to a topic in` topics }
}
@JvmName("areInTheFormat")
infix fun `are in the format`(talkFormat: TalkFormat) {
allSatisfy {
TalkAssert(it) `is in the format` talkFormat
}
}
@JvmName("haveTheirFormatIn")
infix fun `have their format in`(talksFormats: Set<TalkFormat>) {
allSatisfy {
TalkAssert(it) `has its format in` talksFormats
}
}
@JvmName("haveTheirDurationIn")
infix fun `have their duration in `(range: ClosedRange<Duration>) {
allSatisfy {
TalkAssert(it) `has its duration in` range
......@@ -54,28 +48,23 @@ class TalkAssert(actual: Talk) : AbstractAssert<TalkAssert, Talk>(
TalkAssert::class.java
) {
@JvmName("isRelatedToTopic")
infix fun `is related to topic`(topicName: String) {
matches({ it.title.contains(topicName) }, "is related to topic $topicName")
}
@JvmName("isInTheFormat")
infix fun `is in the format`(talkFormat: TalkFormat) {
matches({ it.format == talkFormat }, "correspond explicitly to the format $talkFormat")
}
@JvmName("hasItsFormatIn")
infix fun `has its format in`(talksFormats: Set<TalkFormat>) {
matches({ talksFormats.any { it == actual.format } }, "has its format in the expected ones")
}
@JvmName("hasItsDurationIn")
infix fun `has its duration in`(range: ClosedRange<Duration>) {
matches({ it.duration.coerceIn(range) == it.duration }, "duration is in the expected range")
}
@JvmName("isRelatedToATopicIn")
infix fun `is related to a topic in`(topics: Set<Topic>) {
matches({ topics.any { actual.title.contains(it.name) } }, "is related to a topic in")
}
......
package org.craftsrecords.talkadvisor.recommendation.profile
package org.craftsrecords.talkadvisor.recommendation.criteria
import org.craftsrecords.talkadvisor.recommendation.criteria.Criteria
import org.craftsrecords.talkadvisor.nextString
import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences
import org.craftsrecords.talkadvisor.recommendation.preferences.Topic
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat
......@@ -8,7 +8,7 @@ import kotlin.random.Random
fun createPreferences() =
Preferences(
setOf(Topic(Random.nextInt().toString()), Topic(Random.nextInt().toString())),
setOf(Topic(Random.nextString()), Topic(Random.nextString())),
setOf(TalkFormat.CONFERENCE, TalkFormat.QUICKIE))
fun createCriteria(): Criteria = createPreferences()
package org.craftsrecords.talkadvisor.recommendation.profile
import org.craftsrecords.talkadvisor.nextString
import org.craftsrecords.talkadvisor.recommendation.criteria.createPreferences
import kotlin.random.Random
fun createProfile() = Profile(Random.nextString(), createPreferences())
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.profile
import cucumber.api.CucumberOptions
import cucumber.api.junit.Cucumber
import org.junit.runner.RunWith
@RunWith(Cucumber::class)
@CucumberOptions(
strict = true,
plugin = ["pretty", "html:target/cucumber"],
features = ["classpath:features/profiles.feature"],
glue = ["classpath:org.craftsrecords.talkadvisor.recommendation.stepdefs"])
class ProfileFunctionalTests
\ No newline at end of file
......@@ -2,6 +2,7 @@ package org.craftsrecords.talkadvisor.recommendation.profile
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.craftsrecords.talkadvisor.recommendation.criteria.createPreferences
import org.junit.jupiter.api.Test
internal class ProfileTest {
......
......@@ -6,10 +6,11 @@ import cucumber.api.java.en.When
import org.assertj.core.api.Assertions.assertThat
import org.craftsrecords.talkadvisor.recommendation.api.CreateProfile
import org.craftsrecords.talkadvisor.recommendation.assertions.that
import org.craftsrecords.talkadvisor.recommendation.criteria.createPreferences
import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences
import org.craftsrecords.talkadvisor.recommendation.profile.NoProfileFoundException
import org.craftsrecords.talkadvisor.recommendation.profile.Profile
import org.craftsrecords.talkadvisor.recommendation.profile.createPreferences
import org.craftsrecords.talkadvisor.recommendation.profile.ProfileAlreadyExistsException
import org.craftsrecords.talkadvisor.recommendation.profile.ProfileNotFoundException
import org.craftsrecords.talkadvisor.recommendation.spi.Profiles
class ProfileStepdefs(private val testContext: TestContext,
......@@ -26,6 +27,12 @@ class ProfileStepdefs(private val testContext: TestContext,
testContext.userId = "noProfileUser"
}
@Given("^he already has a profile$")
fun `he already has a profile`() {
val profile = Profile(testContext.userId, createPreferences())
profiles.save(profile)
}
@Given("^he has stored his preferences in his profile$")
fun `he has stored his preferences in his profile`() {
val profile = createProfile(testContext.userId)
......@@ -44,6 +51,15 @@ class ProfileStepdefs(private val testContext: TestContext,
testContext.createdProfile = profile
}
@When("^he tries to create again his profile$")
fun `he tries to create again his profile`() {
try {
createProfile.forUserWithPreferences(testContext.userId, createPreferences())
} catch (e: Exception) {
testContext.error = e
}
}
@Then("^his preferences are stored within$")
fun `his preferences are stored within`() {
val profile = testContext.createdProfile
......@@ -58,8 +74,16 @@ class ProfileStepdefs(private val testContext: TestContext,
fun `he is notified that his profile cannot be found`() {
assertThat(testContext.error)
.isNotNull()
.isInstanceOf(NoProfileFoundException::class.java)
.hasMessage(String.format("No profile found to the user %s", testContext.userId))
.isInstanceOf(ProfileNotFoundException::class.java)
.hasMessage(String.format("No profile found for the user %s", testContext.userId))
}
@Then("^he is notified that his profile already exists$")
fun `he is notified that his profile already exists`() {
assertThat(testContext.error)
.isNotNull()
.isInstanceOf(ProfileAlreadyExistsException::class.java)
.hasMessage(String.format("A profile already exists for the user %s", testContext.userId))
}
private fun createProfile(userId: String): Profile {
......
......@@ -11,3 +11,9 @@ Feature: As a frequent user,
| QUICKIE | CONFERENCE |
When he creates his profile
Then his preferences are stored within
Scenario: A user is trying to create a profile which already exists
Given a user
And he already has a profile
When he tries to create again his profile
Then he is notified that his profile already exists
......@@ -11,26 +11,120 @@
<artifactId>talkadvisor-infra</artifactId>
<properties>
<spring-boot.version>2.1.2.RELEASE</spring-boot.version>
<spring-rest-docs.version>2.0.3.RELEASE</spring-rest-docs.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<configuration>
<compilerPlugins>