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

Adding controllers

parent a7cbbf9c
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
<plugins> <plugins>
<plugin> <plugin>
<artifactId>maven-enforcer-plugin</artifactId> <artifactId>maven-enforcer-plugin</artifactId>
<version>3.0.0-M2</version> <version>3.0.0-M1</version>
<executions> <executions>
<execution> <execution>
<id>dependencies-sanitization</id> <id>dependencies-sanitization</id>
...@@ -93,9 +93,6 @@ ...@@ -93,9 +93,6 @@
<args> <args>
<arg>-Xjsr305=strict</arg> <arg>-Xjsr305=strict</arg>
</args> </args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
<jvmTarget>1.8</jvmTarget> <jvmTarget>1.8</jvmTarget>
<javaParameters>true</javaParameters> <javaParameters>true</javaParameters>
</configuration> </configuration>
......
...@@ -39,6 +39,36 @@ ...@@ -39,6 +39,36 @@
</execution> </execution>
</executions> </executions>
</plugin> </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> </plugins>
</build> </build>
<dependencies> <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 package org.craftsrecords.talkadvisor.recommendation
import org.craftsrecords.hexarch.Aggregate
import org.craftsrecords.talkadvisor.recommendation.criteria.Criteria import org.craftsrecords.talkadvisor.recommendation.criteria.Criteria
import org.craftsrecords.talkadvisor.recommendation.talk.Talk import org.craftsrecords.talkadvisor.recommendation.talk.Talk
import java.util.* import java.util.*
@Aggregate
class Recommendation(val id: UUID = UUID.randomUUID(), talks: Set<Talk>, val criteria: Criteria) { class Recommendation(val id: UUID = UUID.randomUUID(), talks: Set<Talk>, val criteria: Criteria) {
val talks = talks.toSet() val talks = talks.toSet()
......
package org.craftsrecords.talkadvisor.recommendation package org.craftsrecords.talkadvisor.recommendation
import org.craftsrecords.hexarch.DomainService
import org.craftsrecords.talkadvisor.recommendation.api.RecommendTalks import org.craftsrecords.talkadvisor.recommendation.api.RecommendTalks
import org.craftsrecords.talkadvisor.recommendation.criteria.Criteria import org.craftsrecords.talkadvisor.recommendation.criteria.Criteria
import org.craftsrecords.talkadvisor.recommendation.criteria.GuestCriteria 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.Profiles
import org.craftsrecords.talkadvisor.recommendation.spi.Recommendations import org.craftsrecords.talkadvisor.recommendation.spi.Recommendations
import org.craftsrecords.talkadvisor.recommendation.spi.SearchTalks import org.craftsrecords.talkadvisor.recommendation.spi.SearchTalks
import org.craftsrecords.talkadvisor.recommendation.talk.Talk import org.craftsrecords.talkadvisor.recommendation.talk.Talk
@DomainService
class TalksAdvisor(private val searchTalks: SearchTalks, class TalksAdvisor(private val searchTalks: SearchTalks,
private val recommendations: Recommendations, private val recommendations: Recommendations,
private val profiles: Profiles) : RecommendTalks { private val profiles: Profiles) : RecommendTalks {
override fun to(userId: String): Recommendation { override fun to(userId: String): Recommendation {
val profile = profiles.fetch(userId) val profile = profiles.fetch(userId) ?: throw ProfileNotFoundException(userId)
return recommendTalksSatisfying(profile.preferences) return recommendTalksSatisfying(profile.preferences)
} }
...@@ -24,8 +27,7 @@ class TalksAdvisor(private val searchTalks: SearchTalks, ...@@ -24,8 +27,7 @@ class TalksAdvisor(private val searchTalks: SearchTalks,
private fun recommendTalksSatisfying(criteria: Criteria): Recommendation { private fun recommendTalksSatisfying(criteria: Criteria): Recommendation {
val talks = retrieveTalksSatisfying(criteria) val talks = retrieveTalksSatisfying(criteria)
val recommendation = Recommendation(criteria = criteria, talks = talks) val recommendation = Recommendation(criteria = criteria, talks = talks)
recommendations.save(recommendation) return recommendations.save(recommendation)
return recommendation
} }
private fun retrieveTalksSatisfying(criteria: Criteria): Set<Talk> { private fun retrieveTalksSatisfying(criteria: Criteria): Set<Talk> {
......
...@@ -21,6 +21,4 @@ class Profile(id: String, val preferences: Preferences) { ...@@ -21,6 +21,4 @@ class Profile(id: String, val preferences: Preferences) {
override fun hashCode(): Int { override fun hashCode(): Int {
return id.hashCode() 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 package org.craftsrecords.talkadvisor.recommendation.profile
import org.craftsrecords.hexarch.DomainService
import org.craftsrecords.talkadvisor.recommendation.api.CreateProfile import org.craftsrecords.talkadvisor.recommendation.api.CreateProfile
import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences
import org.craftsrecords.talkadvisor.recommendation.spi.Profiles 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 { override fun forUserWithPreferences(userId: String, preferences: Preferences): Profile {
val profile = Profile(userId, preferences) val profile = Profile(userId, preferences)
profiles.save(profile) profiles.fetch(userId)?.let { throw ProfileAlreadyExistsException(userId) }
return profile return profiles.save(profile)
} }
} }
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.profile package org.craftsrecords.talkadvisor.recommendation.profile
class NoProfileFoundException(val userId: String) : RuntimeException("No profile found to the user $userId") class ProfileNotFoundException(val userId: String) : RuntimeException("No profile found for the user $userId")
\ No newline at end of file \ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.spi package org.craftsrecords.talkadvisor.recommendation.spi
import org.craftsrecords.hexarch.Repository
import org.craftsrecords.talkadvisor.recommendation.profile.Profile import org.craftsrecords.talkadvisor.recommendation.profile.Profile
@Repository
interface Profiles { 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 package org.craftsrecords.talkadvisor.recommendation.spi
import org.craftsrecords.hexarch.Repository
import org.craftsrecords.talkadvisor.recommendation.Recommendation import org.craftsrecords.talkadvisor.recommendation.Recommendation
@Repository
interface Recommendations { interface Recommendations {
fun save(recommendation: Recommendation) fun save(recommendation: Recommendation): Recommendation
} }
package org.craftsrecords.talkadvisor.recommendation.spi.stubs package org.craftsrecords.talkadvisor.recommendation.spi.stubs
import org.craftsrecords.hexarch.Stub
import org.craftsrecords.talkadvisor.recommendation.preferences.Topic import org.craftsrecords.talkadvisor.recommendation.preferences.Topic
import org.craftsrecords.talkadvisor.recommendation.spi.SearchTalks import org.craftsrecords.talkadvisor.recommendation.spi.SearchTalks
import org.craftsrecords.talkadvisor.recommendation.talk.Talk import org.craftsrecords.talkadvisor.recommendation.talk.Talk
import java.time.Duration import java.time.Duration
import kotlin.random.Random import kotlin.random.Random
@Stub
class HardCodedTalksSearcher : SearchTalks { class HardCodedTalksSearcher : SearchTalks {
override fun forTopics(topics: Set<Topic>): Set<Talk> { override fun forTopics(topics: Set<Topic>): Set<Talk> {
return createTalks(topics) return createTalks(topics)
......
package org.craftsrecords.talkadvisor.recommendation.spi.stubs 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.profile.Profile
import org.craftsrecords.talkadvisor.recommendation.spi.Profiles import org.craftsrecords.talkadvisor.recommendation.spi.Profiles
@Stub
class InMemoryProfiles(private val profiles: MutableMap<String, Profile> = HashMap()) : Profiles { class InMemoryProfiles(private val profiles: MutableMap<String, Profile> = HashMap()) : Profiles {
override fun fetch(userId: String): Profile { override fun fetch(userId: String): Profile? {
return profiles[userId] ?: throw NoProfileFoundException(userId) return profiles[userId]
} }
override fun save(profile: Profile) { override fun save(profile: Profile): Profile {
profiles[profile.id] = profile profiles[profile.id] = profile
return profile
} }
} }
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.spi.stubs package org.craftsrecords.talkadvisor.recommendation.spi.stubs
import org.craftsrecords.hexarch.Stub
import org.craftsrecords.talkadvisor.recommendation.Recommendation import org.craftsrecords.talkadvisor.recommendation.Recommendation
import org.craftsrecords.talkadvisor.recommendation.spi.Recommendations import org.craftsrecords.talkadvisor.recommendation.spi.Recommendations
import java.util.* import java.util.*
@Stub
class InMemoryRecommendations(private val recommendations: MutableMap<UUID, Recommendation> = HashMap()) : Recommendations { 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 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.CucumberOptions
import cucumber.api.junit.Cucumber import cucumber.api.junit.Cucumber
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(Cucumber::class) @RunWith(Cucumber::class)
@CucumberOptions(strict = true, plugin = ["pretty", "html:target/cucumber"], features = ["classpath:features/"]) @CucumberOptions(strict = true, plugin = ["pretty", "html:target/cucumber"], features = ["classpath:features/recommendations.feature"])
class RunCucumberTest class RecommendationFunctionalTests
\ No newline at end of file \ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation package org.craftsrecords.talkadvisor.recommendation
import org.assertj.core.api.Assertions.assertThat 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.createTalk
import org.craftsrecords.talkadvisor.recommendation.talk.createTalks import org.craftsrecords.talkadvisor.recommendation.talk.createTalks
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
......
...@@ -8,12 +8,10 @@ class ProfileAssert(actual: Profile) : AbstractAssert<ProfileAssert, Profile>( ...@@ -8,12 +8,10 @@ class ProfileAssert(actual: Profile) : AbstractAssert<ProfileAssert, Profile>(
actual, actual,
ProfileAssert::class.java ProfileAssert::class.java
) { ) {
@JvmName("correspondsToUser")
infix fun `corresponds to user`(userId: String) { infix fun `corresponds to user`(userId: String) {
matches({ actual.id == userId }, "profile id corresponds to userId") matches({ actual.id == userId }, "profile id corresponds to userId")
} }
@JvmName("hasPreferences")
infix fun `has preferences`(preferences: Preferences) { infix fun `has preferences`(preferences: Preferences) {
matches({ actual.preferences == preferences }, "matching expected preferences") matches({ actual.preferences == preferences }, "matching expected preferences")
} }
......
...@@ -12,12 +12,10 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati ...@@ -12,12 +12,10 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
RecommendationAssert::class.java RecommendationAssert::class.java
) { ) {
@JvmName("hasTalks")
fun hasTalks() { fun hasTalks() {
matches({ it.talks.isNotEmpty() }, "has talks") matches({ it.talks.isNotEmpty() }, "has talks")
} }
@JvmName("hasTalksRelatedTo")
infix fun `has talks related to`(topicName: String) { infix fun `has talks related to`(topicName: String) {
matches({ matches({
it.criteria.topics.any { topic -> topic.name == topicName } it.criteria.topics.any { topic -> topic.name == topicName }
...@@ -26,7 +24,6 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati ...@@ -26,7 +24,6 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
actual.talks.those `are related to topic` topicName actual.talks.those `are related to topic` topicName
} }
@JvmName("hasTalksRelatedTo")
infix fun `has talks related to`(topics: Set<Topic>) { infix fun `has talks related to`(topics: Set<Topic>) {
matches({ matches({
it.criteria.topics.all { topic -> topics.contains(topic) } it.criteria.topics.all { topic -> topics.contains(topic) }
...@@ -35,7 +32,6 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati ...@@ -35,7 +32,6 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
actual.talks.those `are related to topics` topics actual.talks.those `are related to topics` topics
} }
@JvmName("hasOnlyTalksInTheFormat")
infix fun `has only talks in the format`(talkFormat: TalkFormat) { infix fun `has only talks in the format`(talkFormat: TalkFormat) {
matches({ matches({
it.criteria.talksFormats.all { format -> format == talkFormat } it.criteria.talksFormats.all { format -> format == talkFormat }
...@@ -44,12 +40,10 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati ...@@ -44,12 +40,10 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
actual.talks.those `are in the format` talkFormat actual.talks.those `are in the format` talkFormat
} }
@JvmName("hasOnlyTalksHavingTheirDurationIn")
infix fun `has only talks having their duration in`(range: ClosedRange<Duration>) { infix fun `has only talks having their duration in`(range: ClosedRange<Duration>) {
actual.talks.those `have their duration in ` range actual.talks.those `have their duration in ` range
} }
@JvmName("hasOnlyTalksInTheFormats")
infix fun `has only talks in the formats`(talksFormats: Set<TalkFormat>) { infix fun `has only talks in the formats`(talksFormats: Set<TalkFormat>) {
matches({ matches({
it.criteria.talksFormats.all { talkFormat -> talksFormats.contains(talkFormat) } it.criteria.talksFormats.all { talkFormat -> talksFormats.contains(talkFormat) }
...@@ -57,7 +51,6 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati ...@@ -57,7 +51,6 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
actual.talks.those `have their format in` talksFormats actual.talks.those `have their format in` talksFormats
} }
@JvmName("correspondsToTheCriteria")
infix fun `corresponds to the criteria`(criteria: Criteria) { infix fun `corresponds to the criteria`(criteria: Criteria) {
matches({ it.criteria == criteria }, "corresponds to the given criteria") matches({ it.criteria == criteria }, "corresponds to the given criteria")
} }
......
...@@ -16,32 +16,26 @@ class TalksAssert(actual: Iterable<Talk>) : AbstractIterableAssert<TalksAssert, ...@@ -16,32 +16,26 @@ class TalksAssert(actual: Iterable<Talk>) : AbstractIterableAssert<TalksAssert,
return TalkAssert(talk).`as`(description) return TalkAssert(talk).`as`(description)
} }
@JvmName("areRelatedToTopic")
infix fun `are related to topic`(topicName: String) { infix fun `are related to topic`(topicName: String) {
allSatisfy { TalkAssert(it) `is related to topic` topicName } allSatisfy { TalkAssert(it) `is related to topic` topicName }
} }
@JvmName("areRelatedToTopic")
infix fun `are related to topics`(topics: Set<Topic>) { infix fun `are related to topics`(topics: Set<Topic>) {
allSatisfy { TalkAssert(it) `is related to a topic in` topics } allSatisfy { TalkAssert(it) `is related to a topic in` topics }
} }
@JvmName("areInTheFormat")
infix fun `are in the format`(talkFormat: TalkFormat) { infix fun `are in the format`(talkFormat: TalkFormat) {
allSatisfy { allSatisfy {
TalkAssert(it) `is in the format` talkFormat TalkAssert(it) `is in the format` talkFormat
} }
} }
@JvmName("haveTheirFormatIn")
infix fun `have their format in`(talksFormats: Set<TalkFormat>) { infix fun `have their format in`(talksFormats: Set<TalkFormat>) {
allSatisfy { allSatisfy {
TalkAssert(it) `has its format in` talksFormats TalkAssert(it) `has its format in` talksFormats
} }
}