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

Create Profile Feature

parent 03bda253
package org.craftsrecords.talkadvisor.recommendation.api;
import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences;
import org.craftsrecords.talkadvisor.recommendation.profile.Profile;
import javax.annotation.Nonnull;
@FunctionalInterface
public interface CreateProfile {
@Nonnull
Profile forUserWithPreferences(@Nonnull String userId, @Nonnull Preferences preferences);
}
package org.craftsrecords.talkadvisor.recommendation.spi;
import org.craftsrecords.talkadvisor.recommendation.profile.Profile;
public interface Profiles {
void save(Profile profile);
}
......@@ -9,5 +9,5 @@ import java.util.Set;
@FunctionalInterface
public interface SearchTalks {
@Nonnull
Set<Talk> forTopic(@Nonnull Topic topic);
Set<Talk> forTopics(@Nonnull Set<Topic> topics);
}
......@@ -16,7 +16,7 @@ class TalksAdvisor(private val searchTalks: SearchTalks, private val recommendat
}
private fun getTalksSatisfying(preferences: Preferences): Set<Talk> {
return searchTalks.forTopic(preferences.topic)
return searchTalks.forTopics(preferences.topics)
.filter { preferences.talksFormats.contains(it.format) }
.toSet()
}
......
......@@ -2,6 +2,27 @@ package org.craftsrecords.talkadvisor.recommendation.preferences
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat
class Preferences(val topic: Topic, talksFormats: Set<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
}
}
\ No newline at end of file
......@@ -4,4 +4,19 @@ import org.apache.commons.lang3.Validate.notBlank
class Topic(name: String) {
val name: String = notBlank(name, "Cannot create a topic with a blank name")
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Topic
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
return name.hashCode()
}
}
package org.craftsrecords.talkadvisor.recommendation.profile
import org.apache.commons.lang3.Validate.notBlank
import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences
class Profile(id: String, val preferences: Preferences) {
val id = notBlank(id, "Cannot create a Profile is a blank id")
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Profile
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
return id.hashCode()
}
}
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.profile
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 {
override fun forUserWithPreferences(userId: String, preferences: Preferences): Profile {
val profile = Profile(userId, preferences)
profiles.save(profile)
return profile
}
}
\ No newline at end of file
......@@ -7,16 +7,20 @@ import java.time.Duration
import kotlin.random.Random
class HardCodedTalksSearcher : SearchTalks {
override fun forTopic(topic: Topic): Set<Talk> {
return createTalks(topic)
override fun forTopics(topics: Set<Topic>): Set<Talk> {
return createTalks(topics)
}
private fun createTalks(topic: Topic): Set<Talk> {
return (1..20)
private fun createTalks(topics: Set<Topic>): Set<Talk> {
return topics.flatMap { createTalksForTopic(it.name) }.shuffled().toSet()
}
private fun createTalksForTopic(topicName: String): Set<Talk> {
return (1..30)
.map {
Talk.with {
id = it.toString()
title = "${randomText()} ${topic.name} ${randomText()}"
title = "${randomText()} $topicName ${randomText()}"
duration = Duration.ofMinutes(Random.nextLong(2, 100))
}.build()
}.toSet()
......
package org.craftsrecords.talkadvisor.recommendation.spi.stubs
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 save(profile: Profile) {
profiles[profile.id] = profile
}
}
\ No newline at end of file
......@@ -9,8 +9,8 @@ class Talk private constructor(id: String,
title: String,
duration: Duration) {
val id = notBlank(id, "Cannot create a Talk is a blank id")
val title = notBlank(title, "Cannot create a Talk is a blank title")
val id = notBlank(id, "Cannot create a Talk is a blank id")!!
val title = notBlank(title, "Cannot create a Talk is a blank title")!!
val duration = notNegative(duration)
val format = TalkFormat.ofDuration(duration)
......
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 PreferencesStepdefs implements En {
public PreferencesStepdefs(TestContext testContext) {
Given("^a guest user who wants to learn (.+)",
(String topicName) -> testContext.setRequestedTopic(new Topic(topicName)));
(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("^a user$", () -> {
testContext.setUserId("frequentUser");
});
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.Profile;
import static org.craftsrecords.talkadvisor.recommendation.assertions.TalkAdvisorAssertions.assertThat;
public class ProfileStepdefs implements En {
public ProfileStepdefs(TestContext testContext, CreateProfile createProfile) {
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());
});
}
}
......@@ -42,6 +42,6 @@ public class RecommendationStepdefs implements En {
}
private Preferences createCriteriaFrom(TestContext testContext) {
return new Preferences(testContext.getRequestedTopic(), testContext.getRequestedTalksFormats());
return new Preferences(testContext.getRequestedTopics(), testContext.getRequestedTalksFormats());
}
}
......@@ -2,7 +2,9 @@ package org.craftsrecords.talkadvisor
import cucumber.runtime.java.picocontainer.PicoFactory
import org.craftsrecords.talkadvisor.recommendation.TalksAdvisor
import org.craftsrecords.talkadvisor.recommendation.profile.ProfileCreator
import org.craftsrecords.talkadvisor.recommendation.spi.stubs.HardCodedTalksSearcher
import org.craftsrecords.talkadvisor.recommendation.spi.stubs.InMemoryProfiles
import org.craftsrecords.talkadvisor.recommendation.spi.stubs.InMemoryRecommendations
import org.craftsrecords.talkadvisor.recommendation.stepdefs.TestContext
......@@ -12,5 +14,7 @@ class CustomPicoFactory : PicoFactory() {
addClass(TalksAdvisor::class.java)
addClass(HardCodedTalksSearcher::class.java)
addClass(InMemoryRecommendations::class.java)
addClass(ProfileCreator::class.java)
addClass(InMemoryProfiles::class.java)
}
}
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.assertions
import org.assertj.core.api.AbstractAssert
import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences
import org.craftsrecords.talkadvisor.recommendation.profile.Profile
class ProfileAssert(actual: Profile) : AbstractAssert<ProfileAssert, Profile>(
actual,
ProfileAssert::class.java
) {
@JvmName("correspondToUser")
fun `correspond to user`(userId: String) {
matches({ actual.id == userId }, "profile id corresponds to userId")
}
@JvmName("hasPreferences")
fun `has preferences`(preferences: Preferences) {
matches({ actual.preferences == preferences }, "matching expected preferences")
}
}
......@@ -2,6 +2,7 @@ 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() {
......@@ -14,5 +15,8 @@ class TalkAdvisorAssertions : Assertions() {
@JvmStatic
fun assertThat(actual: Iterable<Talk>) = TalksAssert(actual)
@JvmStatic
fun assertThat(actual: Profile) = ProfileAssert(actual)
}
}
\ No newline at end of file
......@@ -11,11 +11,40 @@ internal class PreferencesTest {
fun `should store a copy of the talks formats`() {
val talksFormats = mutableSetOf(CONFERENCE)
val criteria = Preferences(Topic("ddd"), talksFormats)
val preferences = Preferences(setOf(Topic("ddd")), talksFormats)
talksFormats.add(QUICKIE)
assertThat(criteria.talksFormats).containsOnly(CONFERENCE)
assertThat(preferences.talksFormats).containsOnly(CONFERENCE)
}
@Test
fun `should store a copy of the topics`() {
val topics = mutableSetOf(Topic("ddd"))
val preferences = Preferences(topics, setOf(CONFERENCE))
val newTopic = Topic("new")
topics.add(newTopic)
assertThat(preferences.topics).doesNotContain(newTopic)
}
@Test
fun `should satisfy value object equality`() {
val preferences = Preferences(setOf(Topic("ddd")), setOf(CONFERENCE))
val preferences2 = Preferences(setOf(Topic("ddd")), setOf(CONFERENCE))
assertThat(preferences).isEqualTo(preferences2)
assertThat(preferences.hashCode()).isEqualTo(preferences2.hashCode())
}
@Test
fun `should satisfy value object inequality`() {
val preferences = Preferences(setOf(Topic("ddd")), setOf(CONFERENCE))
val preferences2 = Preferences(setOf(Topic("ddd")), setOf(QUICKIE))
assertThat(preferences).isNotEqualTo(preferences2)
assertThat(preferences.hashCode()).isNotEqualTo(preferences2.hashCode())
}
}
package org.craftsrecords.talkadvisor.recommendation.preferences
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.Test
......@@ -11,4 +12,22 @@ internal class TopicTest {
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage("Cannot create a topic with a blank name")
}
@Test
fun `should satisfy value object equality`() {
val topic = Topic("topic")
val topic2 = Topic("topic")
assertThat(topic).isEqualTo(topic2)
assertThat(topic.hashCode()).isEqualTo(topic2.hashCode())
}
@Test
fun `should satisfy value object inequality`() {
val topic = Topic("topic")
val topic2 = Topic("topic2")
assertThat(topic).isNotEqualTo(topic2)
assertThat(topic.hashCode()).isNotEqualTo(topic2.hashCode())
}
}
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.profile
import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences
import org.craftsrecords.talkadvisor.recommendation.preferences.Topic
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat
import kotlin.random.Random
fun createPreferences() =
Preferences(
setOf(Topic(Random.nextInt().toString()), Topic(Random.nextInt().toString())),
setOf(TalkFormat.CONFERENCE, TalkFormat.QUICKIE))
package org.craftsrecords.talkadvisor.recommendation.profile
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.Test
internal class ProfileTest {
@Test
fun `should not create a profile with a blank id`() {
assertThatThrownBy { Profile("", createPreferences()) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage("Cannot create a Profile is a blank id")
}
@Test
fun `should satisfy entity equality`() {
val profile1 = Profile("id", createPreferences())
val profile2 = Profile("id", createPreferences())
assertThat(profile1).isEqualTo(profile2)
assertThat(profile1.hashCode()).isEqualTo(profile2.hashCode())
}
@Test
fun `should satisfy entity inequality`() {
val profile1 = Profile("id", createPreferences())
val profile2 = Profile("id2", createPreferences())
assertThat(profile1).isNotEqualTo(profile2)
assertThat(profile1.hashCode()).isNotEqualTo(profile2.hashCode())
}
}
\ No newline at end of file
package org.craftsrecords.talkadvisor.recommendation.stepdefs
import org.craftsrecords.talkadvisor.recommendation.Recommendation
import org.craftsrecords.talkadvisor.recommendation.preferences.Preferences
import org.craftsrecords.talkadvisor.recommendation.preferences.Topic
import org.craftsrecords.talkadvisor.recommendation.profile.Profile
import org.craftsrecords.talkadvisor.recommendation.talk.TalkFormat
class TestContext {
lateinit var requestedTopic: Topic
lateinit var requestedTopics: Set<Topic>
lateinit var requestedTalksFormats: Set<TalkFormat>
lateinit var recommendationResult: Recommendation
lateinit var userId: String
lateinit var createdProfile: Profile
lateinit var requestedPreferences: Preferences
}
\ No newline at end of file
Feature: As a frequent user,
In order not repeat my preferences at each request,
I want to create my profile with my preferences
Scenario: The user is creating his profile with his preferences
Given a user
And he wants to learn
| DDD | hexagonal architecture |
And he only wants to see
| QUICKIE | CONFERENCE |
When he creates his profile
Then his preferences are stored within
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment