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

Get recommendations from profile

parent 44bb8468
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.spi;
import org.craftsrecords.talkadvisor.recommendation.Recommendation;
import javax.annotation.Nonnull;
public interface Recommendations {
void save(@Nonnull Recommendation recommendation);
}
package org.craftsrecords.talkadvisor.recommendation.spi;
import org.craftsrecords.talkadvisor.recommendation.preferences.Topic;
import org.craftsrecords.talkadvisor.recommendation.talk.Talk;
import javax.annotation.Nonnull;
import java.util.Set;
@FunctionalInterface
public interface SearchTalks {
@Nonnull
Set<Talk> forTopics(@Nonnull Set<Topic> topics);
}
......@@ -12,25 +12,25 @@ class TalksAdvisor(private val searchTalks: SearchTalks,
private val recommendations: Recommendations,
private val profiles: Profiles) : RecommendTalks {
override fun getRecommendationGivenProfile(userId: String): Recommendation {
override fun to(userId: String): Recommendation {
val profile = profiles.fetch(userId)
return this.getRecommendationSatisfying(profile.preferences)
return recommendTalksSatisfying(profile.preferences)
}
override fun getRecommendationSatisfying(guestCriteria: GuestCriteria): Recommendation {
return this.getRecommendationSatisfying(guestCriteria as Criteria)
override fun satisfying(guestCriteria: GuestCriteria): Recommendation {
return recommendTalksSatisfying(guestCriteria as Criteria)
}
private fun getRecommendationSatisfying(criteria: Criteria): Recommendation {
val talks = getTalksSatisfying(criteria)
private fun recommendTalksSatisfying(criteria: Criteria): Recommendation {
val talks = retrieveTalksSatisfying(criteria)
val recommendation = Recommendation(criteria = criteria, talks = talks)
recommendations.save(recommendation)
return recommendation
}
private fun getTalksSatisfying(criteria: Criteria): Set<Talk> {
private fun retrieveTalksSatisfying(criteria: Criteria): Set<Talk> {
return searchTalks.forTopics(criteria.topics)
.filter { criteria.talksFormats.contains(it.format) }
.filter { criteria.hasTalkFormat(it.format) }
.toSet()
}
}
\ No newline at end of file
......@@ -4,6 +4,6 @@ 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
infix fun satisfying(guestCriteria: GuestCriteria): Recommendation
infix fun to(userId: String): Recommendation
}
\ No newline at end of file
......@@ -6,4 +6,8 @@ import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat
interface Criteria {
val topics: Set<Topic>
val talksFormats: Set<TalkFormat>
fun hasTalkFormat(talkFormat: TalkFormat): Boolean {
return talksFormats.contains(talkFormat)
}
}
\ 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
class NoProfileFoundException(val userId: String) : RuntimeException("No profile found to the user $userId")
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.spi
import org.craftsrecords.talkadvisor.recommendation.profile.Profile
interface Profiles {
fun save(profile: Profile)
fun fetch(userId: String): Profile
}
package org.craftsrecords.talkadvisor.recommendation.spi
import org.craftsrecords.talkadvisor.recommendation.Recommendation
interface Recommendations {
fun save(recommendation: Recommendation)
}
package org.craftsrecords.talkadvisor.recommendation.spi
import org.craftsrecords.talkadvisor.recommendation.preferences.Topic
import org.craftsrecords.talkadvisor.recommendation.talk.Talk
@FunctionalInterface
interface SearchTalks {
fun forTopics(topics: Set<Topic>): Set<Talk>
}
package org.craftsrecords.talkadvisor.recommendation.stepdefs;
import cucumber.api.java8.En;
import io.cucumber.datatable.DataTable;
import org.craftsrecords.talkadvisor.recommendation.preferences.Topic;
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat;
import java.util.Set;
import static java.util.Collections.singleton;
import static java.util.stream.Collectors.toSet;
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))));
Given("^he has only time to watch (.+) talks$",
(String format) -> {
Set<TalkFormat> talkFormats = singleton(TalkFormat.valueOf(format));
testContext.setRequestedTalksFormats(talkFormats);
});
Given("^he wants to learn$", (DataTable topicNames) -> {
Set<Topic> topics =
topicNames.asList()
.stream()
.map(Topic::new)
.collect(toSet());
testContext.setRequestedTopics(topics);
});
Given("^he only wants to see$", (DataTable formats) -> {
Set<TalkFormat> talkFormats =
formats.asList().stream().map(TalkFormat::valueOf).collect(toSet());
testContext.setRequestedTalksFormats(talkFormats);
});
}
}
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, 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);
testContext.setRequestedPreferences(preferences);
testContext.setCreatedProfile(profile);
});
Then("^his preferences are stored within$", () -> {
Profile profile = testContext.getCreatedProfile();
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);
}
}
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.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;
import static java.time.Duration.ofMinutes;
import static kotlin.ranges.RangesKt.rangeTo;
import static org.craftsrecords.talkadvisor.recommendation.assertions.TalkAdvisorAssertions.assertThat;
public class RecommendationStepdefs implements En {
public RecommendationStepdefs(TestContext testContext,
RecommendTalks recommendTalks) {
When("^he asks for a recommendation given his criteria$",
() -> {
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 GuestCriteria createGuestCriteriaFrom(TestContext testContext) {
return new GuestCriteria(testContext.getRequestedTopics(), testContext.getRequestedTalksFormats());
}
}
......@@ -8,13 +8,13 @@ class ProfileAssert(actual: Profile) : AbstractAssert<ProfileAssert, Profile>(
actual,
ProfileAssert::class.java
) {
@JvmName("correspondToUser")
fun `correspond to user`(userId: String) {
@JvmName("correspondsToUser")
infix fun `corresponds to user`(userId: String) {
matches({ actual.id == userId }, "profile id corresponds to userId")
}
@JvmName("hasPreferences")
fun `has preferences`(preferences: Preferences) {
infix fun `has preferences`(preferences: Preferences) {
matches({ actual.preferences == preferences }, "matching expected preferences")
}
}
......@@ -2,7 +2,6 @@ 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
......@@ -14,7 +13,7 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
) {
@JvmName("hasTalks")
fun `has talks`() {
fun hasTalks() {
matches({ it.talks.isNotEmpty() }, "has talks")
}
......@@ -24,7 +23,7 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
it.criteria.topics.any { topic -> topic.name == topicName }
}, "recommendations criteria has the topic $topicName")
assertThat(actual.talks) `are related to topic` topicName
actual.talks.those `are related to topic` topicName
}
@JvmName("hasTalksRelatedTo")
......@@ -33,7 +32,7 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
it.criteria.topics.all { topic -> topics.contains(topic) }
}, "recommendations criteria has the given topics")
assertThat(actual.talks) `are related to topics` topics
actual.talks.those `are related to topics` topics
}
@JvmName("hasOnlyTalksInTheFormat")
......@@ -42,12 +41,12 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
it.criteria.talksFormats.all { format -> format == talkFormat }
}, "recommendations criteria are only of format $talkFormat")
assertThat(actual.talks) `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>) {
assertThat(actual.talks) `have their duration in ` range
actual.talks.those `have their duration in ` range
}
@JvmName("hasOnlyTalksInTheFormats")
......@@ -55,7 +54,7 @@ class RecommendationAssert(actual: Recommendation) : AbstractAssert<Recommendati
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
actual.talks.those `have their format in` talksFormats
}
@JvmName("correspondsToTheCriteria")
......
package org.craftsrecords.talkadvisor.recommendation.assertions
import org.assertj.core.api.Assertions
import org.craftsrecords.talkadvisor.recommendation.Recommendation
import org.craftsrecords.talkadvisor.recommendation.profile.Profile
import org.craftsrecords.talkadvisor.recommendation.talk.Talk
class TalkAdvisorAssertions : Assertions() {
companion object Asserter {
@JvmStatic
fun assertThat(actual: Recommendation) = RecommendationAssert(actual)
val Recommendation.that: RecommendationAssert
get() = RecommendationAssert(this)
@JvmStatic
fun assertThat(actual: Talk) = TalkAssert(actual)
val Talk.that: TalkAssert
get() = TalkAssert(this)
@JvmStatic
fun assertThat(actual: Iterable<Talk>) = TalksAssert(actual)
val Iterable<Talk>.those: TalksAssert
get() = TalksAssert(this)
@JvmStatic
fun assertThat(actual: Profile) = ProfileAssert(actual)
}
}
\ No newline at end of file
val Profile.that: ProfileAssert
get() = ProfileAssert(this)
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.criteria
import org.assertj.core.api.Assertions.assertThat
import org.craftsrecords.talkadvisor.recommendation.preferences.Topic
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat.*
import org.junit.jupiter.api.Test
internal class CriteriaTest {
@Test
fun `should have talkFormat`() {
val talksFormats = setOf(CONFERENCE, UNIVERSITY)
val criteria: Criteria = createCriteria(talksFormats)
assertThat(criteria.hasTalkFormat(CONFERENCE)).isTrue()
}
@Test
fun `should not have talkFormat`() {
val talksFormats = setOf(CONFERENCE, UNIVERSITY)
val criteria: Criteria = createCriteria(talksFormats)
assertThat(criteria.hasTalkFormat(QUICKIE)).isFalse()
}
private fun createCriteria(talksFormats: Set<TalkFormat>): Criteria {
return GuestCriteria(setOf(Topic("ddd")), talksFormats)
}
}
\ No newline at end of file
......@@ -2,7 +2,8 @@ package org.craftsrecords.talkadvisor.recommendation.criteria
import org.assertj.core.api.Assertions.assertThat
import org.craftsrecords.talkadvisor.recommendation.preferences.Topic
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat.CONFERENCE
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat.QUICKIE
import org.junit.jupiter.api.Test
internal class GuestCriteriaTest {
......@@ -10,19 +11,19 @@ internal class GuestCriteriaTest {
@Test
fun `should store a copy of the talks formats`() {
val talksFormats = mutableSetOf(TalkFormat.CONFERENCE)
val talksFormats = mutableSetOf(CONFERENCE)
val guestCriteria = GuestCriteria(setOf(Topic("ddd")), talksFormats)
talksFormats.add(TalkFormat.QUICKIE)
talksFormats.add(QUICKIE)
assertThat(guestCriteria.talksFormats).containsOnly(TalkFormat.CONFERENCE)
assertThat(guestCriteria.talksFormats).containsOnly(CONFERENCE)
}
@Test
fun `should store a copy of the topics`() {
val topics = mutableSetOf(Topic("ddd"))
val guestCriteria = GuestCriteria(topics, setOf(TalkFormat.CONFERENCE))
val guestCriteria = GuestCriteria(topics, setOf(CONFERENCE))
val newTopic = Topic("new")
topics.add(newTopic)
......@@ -31,8 +32,8 @@ internal class GuestCriteriaTest {
@Test
fun `should satisfy value object equality`() {
val guestCriteria = GuestCriteria(setOf(Topic("ddd")), setOf(TalkFormat.CONFERENCE))
val guestCriteria2 = GuestCriteria(setOf(Topic("ddd")), setOf(TalkFormat.CONFERENCE))
val guestCriteria = GuestCriteria(setOf(Topic("ddd")), setOf(CONFERENCE))
val guestCriteria2 = GuestCriteria(setOf(Topic("ddd")), setOf(CONFERENCE))
assertThat(guestCriteria).isEqualTo(guestCriteria2)
assertThat(guestCriteria.hashCode()).isEqualTo(guestCriteria2.hashCode())
......@@ -40,8 +41,8 @@ internal class GuestCriteriaTest {
@Test
fun `should satisfy value object inequality`() {
val guestCriteria = GuestCriteria(setOf(Topic("ddd")), setOf(TalkFormat.CONFERENCE))
val guestCriteria2 = GuestCriteria(setOf(Topic("ddd")), setOf(TalkFormat.QUICKIE))
val guestCriteria = GuestCriteria(setOf(Topic("ddd")), setOf(CONFERENCE))
val guestCriteria2 = GuestCriteria(setOf(Topic("ddd")), setOf(QUICKIE))
assertThat(guestCriteria).isNotEqualTo(guestCriteria2)
assertThat(guestCriteria.hashCode()).isNotEqualTo(guestCriteria2.hashCode())
......
package org.craftsrecords.talkadvisor.recommendation.stepdefs
import cucumber.api.java.en.Given
import org.craftsrecords.talkadvisor.recommendation.preferences.Topic
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat
class CriteriaStepdefs(private val testContext: TestContext) {
@Given("^a guest user who wants to learn (.+)")
fun `a guest user who wants to learn`(topicName: String) {
testContext.requestedTopics = setOf(Topic(topicName))
}
@Given("^he has only time to watch (.+) talks$")
fun `he has only time to watch talks on`(format: String) {
val talkFormats = setOf(TalkFormat.valueOf(format))
testContext.requestedTalksFormats = talkFormats
}
@Given("^he wants to learn$")
fun `he wants to learn`(topicNames: List<String>) {
val topics = topicNames.map { Topic(it) }.toSet()
testContext.requestedTopics = topics
}
@Given("^he only wants to see$")
fun `he only wants to see`(formats: List<String>) {
val talkFormats = formats.map { TalkFormat.valueOf(it) }.toSet()
testContext.requestedTalksFormats = talkFormats
}
}
package org.craftsrecords.talkadvisor.recommendation.stepdefs
import cucumber.api.java.en.Given
import cucumber.api.java.en.Then