Commit 06d27a58 authored by Julien Topçu's avatar Julien Topçu
Browse files

Get recommendations from profile

parent 74910fbd
package org.craftsrecords.talkadvisor.recommendation.api;
import org.craftsrecords.talkadvisor.recommendation.Recommendation;
import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences;
import javax.annotation.Nonnull;
@FunctionalInterface
public interface RecommendTalksForTopic {
@Nonnull
Recommendation getRecommendation(@Nonnull Preferences preferences);
}
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.spi;
import org.craftsrecords.talkadvisor.recommendation.profile.Profile;
import org.jetbrains.annotations.NotNull;
public interface Profiles {
void save(Profile profile);
@NotNull
Profile fetch(@NotNull String userId);
}
package org.craftsrecords.talkadvisor.recommendation
import org.craftsrecords.talkadvisor.recommendation.criteria.Criteria
import org.craftsrecords.talkadvisor.recommendation.talk.Talk
import java.util.*
class Recommendation(val id: UUID = UUID.randomUUID(), talks: Set<Talk>) {
class Recommendation(val id: UUID = UUID.randomUUID(), talks: Set<Talk>, val criteria: Criteria) {
val talks = talks.toSet()
......@@ -23,8 +24,6 @@ class Recommendation(val id: UUID = UUID.randomUUID(), talks: Set<Talk>) {
}
override fun toString(): String {
return "Recommendation(id=$id, talks=$talks)"
return "Recommendation(id=$id, criteria=$criteria, talks=$talks)"
}
}
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation
import org.craftsrecords.talkadvisor.recommendation.api.RecommendTalksForTopic
import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences
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.spi.Profiles
import org.craftsrecords.talkadvisor.recommendation.spi.Recommendations
import org.craftsrecords.talkadvisor.recommendation.spi.SearchTalks
import org.craftsrecords.talkadvisor.recommendation.talk.Talk
class TalksAdvisor(private val searchTalks: SearchTalks, private val recommendations: Recommendations) : RecommendTalksForTopic {
class TalksAdvisor(private val searchTalks: SearchTalks,
private val recommendations: Recommendations,
private val profiles: Profiles) : RecommendTalks {
override fun getRecommendation(preferences: Preferences): Recommendation {
val talks = getTalksSatisfying(preferences)
val recommendation = Recommendation(talks = talks)
override fun getRecommendationGivenProfile(userId: String): Recommendation {
val profile = profiles.fetch(userId)
return this.getRecommendationSatisfying(profile.preferences)
}
override fun getRecommendationSatisfying(guestCriteria: GuestCriteria): Recommendation {
return this.getRecommendationSatisfying(guestCriteria as Criteria)
}
private fun getRecommendationSatisfying(criteria: Criteria): Recommendation {
val talks = getTalksSatisfying(criteria)
val recommendation = Recommendation(criteria = criteria, talks = talks)
recommendations.save(recommendation)
return recommendation
}
private fun getTalksSatisfying(preferences: Preferences): Set<Talk> {
return searchTalks.forTopics(preferences.topics)
.filter { preferences.talksFormats.contains(it.format) }
private fun getTalksSatisfying(criteria: Criteria): Set<Talk> {
return searchTalks.forTopics(criteria.topics)
.filter { criteria.talksFormats.contains(it.format) }
.toSet()
}
}
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.api;
package org.craftsrecords.talkadvisor.recommendation.api
import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences;
import org.craftsrecords.talkadvisor.recommendation.profile.Profile;
import javax.annotation.Nonnull;
import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences
import org.craftsrecords.talkadvisor.recommendation.profile.Profile
@FunctionalInterface
public interface CreateProfile {
@Nonnull
Profile forUserWithPreferences(@Nonnull String userId, @Nonnull Preferences preferences);
interface CreateProfile {
fun forUserWithPreferences(userId: String, preferences: Preferences): Profile
}
package org.craftsrecords.talkadvisor.recommendation.api
import org.craftsrecords.talkadvisor.recommendation.Recommendation
import org.craftsrecords.talkadvisor.recommendation.criteria.GuestCriteria
interface RecommendTalks {
fun getRecommendationSatisfying(guestCriteria: GuestCriteria): Recommendation
fun getRecommendationGivenProfile(userId: String): Recommendation
}
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.criteria
import org.craftsrecords.talkadvisor.recommendation.preferences.Topic
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat
interface Criteria {
val topics: Set<Topic>
val talksFormats: Set<TalkFormat>
}
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.criteria
import org.craftsrecords.talkadvisor.recommendation.preferences.Topic
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat
class GuestCriteria(topics: Set<Topic>, talksFormats: Set<TalkFormat>) : Criteria {
override val talksFormats: Set<TalkFormat> = talksFormats.toSet()
override val topics: Set<Topic> = topics.toSet()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Criteria
if (talksFormats != other.talksFormats) return false
if (topics != other.topics) return false
return true
}
override fun hashCode(): Int {
var result = talksFormats.hashCode()
result = 31 * result + topics.hashCode()
return result
}
}
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.preferences
import org.craftsrecords.talkadvisor.recommendation.criteria.Criteria
import org.craftsrecords.talkadvisor.recommendation.criteria.GuestCriteria
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat
class Preferences(topics: Set<Topic>, talksFormats: Set<TalkFormat>) {
val talksFormats: Set<TalkFormat> = talksFormats.toSet()
val topics: Set<Topic> = topics.toSet()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Preferences
if (talksFormats != other.talksFormats) return false
if (topics != other.topics) return false
return true
}
override fun hashCode(): Int {
var result = talksFormats.hashCode()
result = 31 * result + topics.hashCode()
return result
}
data class Preferences constructor(private val criteria: Criteria) : Criteria by criteria {
constructor(topics: Set<Topic>, talksFormats: Set<TalkFormat>) : this(GuestCriteria(topics, talksFormats))
}
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.profile
class NoProfileFoundException(val userId: String) : RuntimeException("No profile found for the user $userId")
\ No newline at end of file
......@@ -20,11 +20,13 @@ class HardCodedTalksSearcher : SearchTalks {
.map {
Talk.with {
id = it.toString()
title = "${randomText()} $topicName ${randomText()}"
title = generateTalkName(topicName)
duration = Duration.ofMinutes(Random.nextLong(2, 100))
}.build()
}.toSet()
}
private fun generateTalkName(topicName: String) = "${randomText()} $topicName ${randomText()}"
private fun randomText() = Random.nextBits(4).toString()
}
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.spi.stubs
import org.craftsrecords.talkadvisor.recommendation.profile.NoProfileFoundException
import org.craftsrecords.talkadvisor.recommendation.profile.Profile
import org.craftsrecords.talkadvisor.recommendation.spi.Profiles
class InMemoryProfiles(private val profiles: MutableMap<String, Profile> = HashMap()) : Profiles {
override fun fetch(userId: String): Profile {
return profiles[userId] ?: throw NoProfileFoundException(userId)
}
override fun save(profile: Profile) {
profiles[profile.id] = profile
}
}
\ No newline at end of file
......@@ -10,8 +10,8 @@ import java.util.Set;
import static java.util.Collections.singleton;
import static java.util.stream.Collectors.toSet;
public class PreferencesStepdefs implements En {
public PreferencesStepdefs(TestContext testContext) {
public class CriteriaStepdefs implements En {
public CriteriaStepdefs(TestContext testContext) {
Given("^a guest user who wants to learn (.+)",
(String topicName) -> testContext.setRequestedTopics(singleton(new Topic(topicName))));
......@@ -21,10 +21,6 @@ public class PreferencesStepdefs implements En {
testContext.setRequestedTalksFormats(talkFormats);
});
Given("^a user$", () -> {
testContext.setUserId("frequentUser");
});
Given("^he wants to learn$", (DataTable topicNames) -> {
Set<Topic> topics =
topicNames.asList()
......
......@@ -3,12 +3,28 @@ package org.craftsrecords.talkadvisor.recommendation.stepdefs;
import cucumber.api.java8.En;
import org.craftsrecords.talkadvisor.recommendation.api.CreateProfile;
import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences;
import org.craftsrecords.talkadvisor.recommendation.profile.CriteriaFactoryKt;
import org.craftsrecords.talkadvisor.recommendation.profile.NoProfileFoundException;
import org.craftsrecords.talkadvisor.recommendation.profile.Profile;
import org.craftsrecords.talkadvisor.recommendation.spi.Profiles;
import org.jetbrains.annotations.NotNull;
import static org.craftsrecords.talkadvisor.recommendation.assertions.TalkAdvisorAssertions.assertThat;
public class ProfileStepdefs implements En {
public ProfileStepdefs(TestContext testContext, CreateProfile createProfile) {
public ProfileStepdefs(TestContext testContext, CreateProfile createProfile, Profiles profiles) {
Given("^a user$", () -> {
testContext.setUserId("frequentUser");
});
Given("^a user with no profile$", () -> {
testContext.setUserId("noProfileUser");
});
Given("^he has stored his preferences in his profile$", () -> {
Profile profile = createProfile(testContext.userId);
profiles.save(profile);
testContext.setCreatedProfile(profile);
});
When("^he creates his profile$", () -> {
Preferences preferences = new Preferences(testContext.getRequestedTopics(), testContext.getRequestedTalksFormats());
Profile profile = createProfile.forUserWithPreferences(testContext.getUserId(), preferences);
......@@ -21,5 +37,17 @@ public class ProfileStepdefs implements En {
assertThat(profile).correspondToUser(testContext.getUserId());
assertThat(profile).hasPreferences(testContext.getRequestedPreferences());
});
Then("^he is notified that his profile cannot be found$", () -> {
assertThat(testContext.getError())
.isNotNull()
.isInstanceOf(NoProfileFoundException.class)
.hasMessage(String.format("No profile found for the user %s", testContext.userId));
});
}
@NotNull
private Profile createProfile(String userId) {
Preferences preferences = CriteriaFactoryKt.createPreferences();
return new Profile(userId, preferences);
}
}
......@@ -3,8 +3,10 @@ package org.craftsrecords.talkadvisor.recommendation.stepdefs;
import cucumber.api.java8.En;
import kotlin.ranges.ClosedRange;
import org.craftsrecords.talkadvisor.recommendation.Recommendation;
import org.craftsrecords.talkadvisor.recommendation.api.RecommendTalksForTopic;
import org.craftsrecords.talkadvisor.recommendation.api.RecommendTalks;
import org.craftsrecords.talkadvisor.recommendation.criteria.GuestCriteria;
import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences;
import org.craftsrecords.talkadvisor.recommendation.profile.Profile;
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat;
import java.time.Duration;
......@@ -15,33 +17,57 @@ import static org.craftsrecords.talkadvisor.recommendation.assertions.TalkAdviso
public class RecommendationStepdefs implements En {
public RecommendationStepdefs(TestContext testContext,
RecommendTalksForTopic recommendTalksForTopic) {
When("^he asks for the related talks$",
RecommendTalks recommendTalks) {
When("^he asks for a recommendation given his criteria$",
() -> {
Preferences preferences = createCriteriaFrom(testContext);
Recommendation recommendation = recommendTalksForTopic.getRecommendation(preferences);
GuestCriteria guestCriteria = createGuestCriteriaFrom(testContext);
Recommendation recommendation = recommendTalks.getRecommendationSatisfying(guestCriteria);
testContext.setRecommendationResult(recommendation);
});
When("^he asks for a recommendation$", () -> {
try {
Recommendation recommendation = recommendTalks.getRecommendationGivenProfile(testContext.getUserId());
testContext.setRecommendationResult(recommendation);
} catch (Exception e) {
testContext.setError(e);
}
});
Then("^talkadvisor recommends some talks$", () -> {
Recommendation recommendation = testContext.getRecommendationResult();
assertThat(recommendation).hasTalks();
});
Then("^the recommended talks are related to (.+)",
(String topicName) -> {
Recommendation recommendation = testContext.getRecommendationResult();
assertThat(recommendation).hasTalksRelatedTo(topicName);
});
Then("^all the talks corresponding to the (.+) format have a duration between (\\d+) and (\\d+) minutes$",
(String format, Integer minDuration, Integer maxDuration) -> {
Recommendation recommendation = testContext.getRecommendationResult();
TalkFormat talkFormat = TalkFormat.valueOf(format);
ClosedRange<Duration> range = rangeTo(ofMinutes(minDuration), ofMinutes(maxDuration));
assertThat(recommendation).hasOnlyTalksInTheFormat(talkFormat);
assertThat(recommendation).hasOnlyTalksHavingTheirDurationIn(range);
});
Then("^the recommended talks correspond to his preferences$", () -> {
Recommendation recommendation = testContext.getRecommendationResult();
Profile profile = testContext.getCreatedProfile();
Preferences preferences = profile.getPreferences();
assertThat(recommendation).correspondsToTheCriteria(preferences);
assertThat(recommendation).hasTalksRelatedTo(preferences.getTopics());
assertThat(recommendation).hasOnlyTalksInTheFormats(preferences.getTalksFormats());
});
}
private Preferences createCriteriaFrom(TestContext testContext) {
return new Preferences(testContext.getRequestedTopics(), testContext.getRequestedTalksFormats());
private GuestCriteria createGuestCriteriaFrom(TestContext testContext) {
return new GuestCriteria(testContext.getRequestedTopics(), testContext.getRequestedTalksFormats());
}
}
package org.craftsrecords.talkadvisor.recommendation
import org.assertj.core.api.Assertions.assertThat
import org.craftsrecords.talkadvisor.recommendation.profile.createCriteria
import org.craftsrecords.talkadvisor.recommendation.talk.createTalk
import org.craftsrecords.talkadvisor.recommendation.talk.createTalks
import org.junit.jupiter.api.Test
......@@ -10,18 +11,20 @@ internal class RecommendationTest {
@Test
fun `should create a recommendation`() {
val talks = createTalks()
val criteria = createCriteria()
val recommendation = Recommendation(talks = talks)
val recommendation = Recommendation(criteria = criteria, talks = talks)
assertThat(recommendation.id).isNotNull()
assertThat(recommendation.talks).isEqualTo(talks)
assertThat(recommendation.criteria).isEqualTo(criteria)
}
@Test
fun `should store a copy of the talks`() {
val talks = createTalks().toMutableSet()
val recommendation = Recommendation(talks = talks)
val recommendation = Recommendation(criteria = createCriteria(), talks = talks)
val newTalk = createTalk()
talks.add(newTalk)
......@@ -33,8 +36,8 @@ internal class RecommendationTest {
fun `should satisfy entity equality`() {
val id = UUID.randomUUID()
val recommendation1 = Recommendation(id, talks = createTalks())
val recommendation2 = Recommendation(id, talks = createTalks())
val recommendation1 = Recommendation(id, criteria = createCriteria(), talks = createTalks())
val recommendation2 = Recommendation(id, criteria = createCriteria(), talks = createTalks())
assertThat(recommendation1).isEqualTo(recommendation2)
assertThat(recommendation1.hashCode()).isEqualTo(recommendation2.hashCode())
......@@ -44,9 +47,10 @@ internal class RecommendationTest {
fun `should satisfy entity inequality`() {
val talks = createTalks()
val criteria = createCriteria()
val recommendation1 = Recommendation(talks = talks)
val recommendation2 = Recommendation(talks = talks)
val recommendation1 = Recommendation(criteria = criteria, talks = talks)
val recommendation2 = Recommendation(criteria = criteria, talks = talks)
assertThat(recommendation1).isNotEqualTo(recommendation2)
assertThat(recommendation1.hashCode()).isNotEqualTo(recommendation2.hashCode())
......
......@@ -3,6 +3,8 @@ package org.craftsrecords.talkadvisor.recommendation.assertions
import org.assertj.core.api.AbstractAssert
import org.craftsrecords.talkadvisor.recommendation.Recommendation
import org.craftsrecords.talkadvisor.recommendation.assertions.TalkAdvisorAssertions.Asserter.assertThat
import org.craftsrecords.talkadvisor.recommendation.criteria.Criteria
import org.craftsrecords.talkadvisor.recommendation.preferences.Topic
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat
import java.time.Duration
......@@ -18,16 +20,46 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
@JvmName("hasTalksRelatedTo")
infix fun `has talks related to`(topicName: String) {
matches({
it.criteria.topics.any { topic -> topic.name == topicName }
}, "recommendations criteria has the topic $topicName")
assertThat(actual.talks) `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) }
}, "recommendations criteria has the given topics")
assertThat(actual.talks) `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 }
}, "recommendations criteria are only of format $talkFormat")
assertThat(actual.talks) `are in the format` talkFormat
}
@JvmName("hasOnlyTalksHavingTheirDurationIn")
fun `has only talks having their duration in`(range: ClosedRange<Duration>) {
infix fun `has only talks having their duration in`(range: ClosedRange<Duration>) {
assertThat(actual.talks) `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) }
}, "recommendations criteria are talks format in the expected ones")
assertThat(actual.talks) `have their format in` talksFormats
}
@JvmName("correspondsToTheCriteria")
infix fun `corresponds to the criteria`(criteria: Criteria) {
matches({ it.criteria == criteria }, "corresponds to the given criteria")
}
}
\ No newline at end of file
......@@ -2,6 +2,7 @@ package org.craftsrecords.talkadvisor.recommendation.assertions
import org.assertj.core.api.AbstractAssert
import org.assertj.core.api.AbstractIterableAssert
import org.craftsrecords.talkadvisor.recommendation.preferences.Topic
import org.craftsrecords.talkadvisor.recommendation.talk.Talk
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat
import java.time.Duration
......@@ -20,6 +21,12 @@ class TalksAssert(actual: Iterable<Talk>) : AbstractIterableAssert<TalksAssert,
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 {
......@@ -27,6 +34,14 @@ class TalksAssert(actual: Iterable<Talk>) : AbstractIterableAssert<TalksAssert,
}
}
@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
......@@ -49,8 +64,19 @@ class TalkAssert(actual: Talk) : AbstractAssert<TalkAssert, Talk>(
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")