diff --git a/src/main/scala/ru/otus/sc/App.scala b/src/main/scala/ru/otus/sc/App.scala index a110bab..549c676 100644 --- a/src/main/scala/ru/otus/sc/App.scala +++ b/src/main/scala/ru/otus/sc/App.scala @@ -1,22 +1,209 @@ package ru.otus.sc -import ru.otus.sc.greet.dao.impl.GreetingDaoImpl +import ru.otus.sc.Engine.{Greeted, StorageKey, StorageValue} +import ru.otus.sc.countdown.model._ +import ru.otus.sc.counter.model._ +import ru.otus.sc.echo.model.{EchoRequest, EchoResponse} import ru.otus.sc.greet.model.{GreetRequest, GreetResponse} -import ru.otus.sc.greet.service.GreetingService -import ru.otus.sc.greet.service.impl.GreetingServiceImpl +import ru.otus.sc.reverse.model.{ReverseRequest, ReverseResponse} +import ru.otus.sc.storage.model._ +import ru.otus.sc.sum.model.{SumRequest, SumResponse} +import ru.otus.sc.user.model._ trait App { - def greet(request: GreetRequest): GreetResponse + // reply on requested value with the same value + // echo value can be multiplied up to 5 times with `repeatNum` value + // no multiply answer by default (`repeatNum` is 1) + // proper values return answer with `EchoAnswerResponse` + // bad values return error with `EchoErrorResponse` + def echo(request: EchoRequest): EchoResponse + // greet with provided object + // panic if `isHuman` flag is unset + // greeting request requires implicit parameter for extracting name from the object + // in current implementation object is User and implicit can be borrowed from + // `ru.otus.sc.user.implicits.UserNameImplicits._` + def greet(request: GreetRequest[Greeted]): GreetResponse + // reverse given string from end to begin + def reverse(request: ReverseRequest): ReverseResponse + // sum 2 given values locally + // sum can be performed externally with `external` flag + // in current implementation external computation is artificial delay + // which was implemented with lazy value + def sum(request: SumRequest): SumResponse + // manage users + // user is compound from: + // id, which is DB related stuff like pk + // unique id, UUID, this item is unique for whole system + // user name, which is compound from: first, last, middle, patronymic names and title + // age + // set of tags + // CRUD operations are available as well as search for first, last name and tag + def createUser(request: CreateUserRequest): CreateUserResponse + def getUser(request: GetUserRequest): GetUserResponse + def deleteUser(request: DeleteUserRequest): DeleteUserResponse + def updateUser(request: UpdateUserRequest): UpdateUserResponse + def findUsers(request: FindUsersRequest): FindUsersResponse + // manage user tags + // tags are bound with users and related only them + // tag is compound from: + // id, which is DB related stuff like pk + // name, for human + // CRUD operations are available as well as search for name + // also associated with certain tag users can be obtained in search request + // tags and users are totally independent + // tagging and untagging users do not allow manage tags on users directly + // and have to be performed as separate operations for users and tags + // e.g. tagging user example, + // imagine we already have app, user, tag and all operations are done successfully: + // val tagId = 42L + // val uniqueUserId = UUID.fromString("4a71a58b-4b39-44fc-ae52-76b9657be280") + // val tag = app.getUserTag(GetUserTagRequest(tagId)) + // val user = app.getUser(GetUserRequest(uniqueUserId)) + // val updatedUser = user.copy(tags = (user.tags + tag)) + // app.tagUser(UpdateTagUserRequest(tagId, uniqueUserId)) + // app.updateUser(UpdateUserRequest(updatedUser)) + def createUserTag(request: CreateUserTagRequest): CreateUserTagResponse + def getUserTag(request: GetUserTagRequest): GetUserTagResponse + def deleteUserTag(request: DeleteUserTagRequest): DeleteUserTagResponse + def updateUserTag(request: UpdateUserTagRequest): UpdateUserTagResponse + def findUserTags(request: FindUserTagsRequest): FindUserTagsResponse + def tagUser(request: UpdateTagUserRequest): UpdateTagUserResponse + def untagUser(request: UpdateUntagUserRequest): UpdateUntagUserResponse + // manage counters + // counter is auto-increment item with initial `value`, default value is 1 + // update increments counter by 1 + // counter value can not be updated itself, it can be only incremented + // timestamp is updated on every counter update + // counter is compound from: + // id, UUID, this item is unique for whole system + // timestamp of update + // value of counter + // CRUD operations are available as well as search for values or timestamps + // searching requires predicate (t: T, i: T) => Boolean for comparing items + // where `t` is target and `i` is item in DB + def createCounter(request: CreateCounterRequest): CreateCounterResponse + def deleteCounter(request: DeleteCounterRequest): DeleteCounterResponse + def updateCounter(request: UpdateCounterRequest): UpdateCounterResponse + def getCounter(request: GetCounterRequest): GetCounterResponse + def findCounters(request: FindCountersRequest): FindCountersResponse + // manage countdowns + // countdown is auto-decrement item with initial `value`, default value is 1 + // update auto-decreases value by 1 + // when countdown reaches 0 it stops decrease value and swaps state from Tick to Done + // Countdown.Done is composed from id, UUID, this item is unique for whole system + // State above can not be updated anymore, also it can be created `CountdownDone` + // Countdown.Tick is composed from: + // id, UUID, this item is unique for whole system + // updater, UUID this is id of something that create or update countdown + // value of countdown + // CRUD operations are available as well as search for: + // values, updaters, Done and NonDone states + // searching for values requires predicate (t: T, i: T) => Boolean for comparing items + // where `t` is target and `i` is item in DB + def createCountdown(request: CreateCountdownRequest): CreateCountdownResponse + def deleteCountdown(request: DeleteCountdownRequest): DeleteCountdownResponse + def updateCountdown(request: UpdateCountdownRequest): UpdateCountdownResponse + def getCountdown(request: GetCountdownRequest): GetCountdownResponse + def findCountdowns(request: FindCountdownsRequest): FindCountdownsResponse + // manage storage + // provide value by requested key + // StorageEntry is composed from key and value of defined types + // in current implementation keys and values are String + // CRUD operations are available as well as search for value itself or predicate on value + // searching with predicate requires predicate i: V => Boolean for comparing items + // where `i` is item in DB + def createStorage( + request: CreateStorageRequest[StorageKey, StorageValue] + ): CreateStorageResponse[StorageKey, StorageValue] + def getStorage( + request: GetStorageRequest[StorageKey, StorageValue] + ): GetStorageResponse[StorageKey, StorageValue] + def deleteStorage( + request: DeleteStorageRequest[StorageKey, StorageValue] + ): DeleteStorageResponse[StorageKey, StorageValue] + def updateStorage( + request: UpdateStorageRequest[StorageKey, StorageValue] + ): UpdateStorageResponse[StorageKey, StorageValue] + def findStorages( + request: FindStoragesRequest[StorageKey, StorageValue] + ): FindStoragesResponse[StorageKey, StorageValue] } object App { - private class AppImpl(greeting: GreetingService) extends App { - def greet(request: GreetRequest): GreetResponse = greeting.greet(request) - } + def apply(): App = new AppImpl(Engine()) - def apply(): App = { - val greetingDao = new GreetingDaoImpl - val greetingService = new GreetingServiceImpl(greetingDao) - new AppImpl(greetingService) + private class AppImpl(engine: Engine) extends App { + def echo(request: EchoRequest): EchoResponse = + engine.echoing.echo(request) + def greet(request: GreetRequest[Greeted]): GreetResponse = + engine.greeting.greet(request) + def reverse(request: ReverseRequest): ReverseResponse = + engine.reversing.reverse(request) + def sum(request: SumRequest): SumResponse = + engine.summing.sum(request) + def createUser(request: CreateUserRequest): CreateUserResponse = + engine.usering.createUser(request) + def getUser(request: GetUserRequest): GetUserResponse = + engine.usering.getUser(request) + def deleteUser(request: DeleteUserRequest): DeleteUserResponse = + engine.usering.deleteUser(request) + def updateUser(request: UpdateUserRequest): UpdateUserResponse = + engine.usering.updateUser(request) + def findUsers(request: FindUsersRequest): FindUsersResponse = + engine.usering.findUsers(request) + def createUserTag(request: CreateUserTagRequest): CreateUserTagResponse = + engine.userTagging.createUserTag(request) + def getUserTag(request: GetUserTagRequest): GetUserTagResponse = + engine.userTagging.getUserTag(request) + def deleteUserTag(request: DeleteUserTagRequest): DeleteUserTagResponse = + engine.userTagging.deleteUserTag(request) + def updateUserTag(request: UpdateUserTagRequest): UpdateUserTagResponse = + engine.userTagging.updateUserTag(request) + def findUserTags(request: FindUserTagsRequest): FindUserTagsResponse = + engine.userTagging.findUserTags(request) + def tagUser(request: UpdateTagUserRequest): UpdateTagUserResponse = + engine.userTagging.tagUser(request) + def untagUser(request: UpdateUntagUserRequest): UpdateUntagUserResponse = + engine.userTagging.untagUser(request) + def createCounter(request: CreateCounterRequest): CreateCounterResponse = + engine.counting.createCounter(request) + def deleteCounter(request: DeleteCounterRequest): DeleteCounterResponse = + engine.counting.deleteCounter(request) + def updateCounter(request: UpdateCounterRequest): UpdateCounterResponse = + engine.counting.updateCounter(request) + def getCounter(request: GetCounterRequest): GetCounterResponse = + engine.counting.getCounter(request) + def findCounters(request: FindCountersRequest): FindCountersResponse = + engine.counting.findCounters(request) + def createCountdown(request: CreateCountdownRequest): CreateCountdownResponse = + engine.countdowning.createCountdown(request) + def deleteCountdown(request: DeleteCountdownRequest): DeleteCountdownResponse = + engine.countdowning.deleteCountdown(request) + def updateCountdown(request: UpdateCountdownRequest): UpdateCountdownResponse = + engine.countdowning.updateCountdown(request) + def getCountdown(request: GetCountdownRequest): GetCountdownResponse = + engine.countdowning.getCountdown(request) + def findCountdowns(request: FindCountdownsRequest): FindCountdownsResponse = + engine.countdowning.findCountdowns(request) + def createStorage( + request: CreateStorageRequest[StorageKey, StorageValue] + ): CreateStorageResponse[StorageKey, StorageValue] = + engine.getting.createStorage(request) + def deleteStorage( + request: DeleteStorageRequest[StorageKey, StorageValue] + ): DeleteStorageResponse[StorageKey, StorageValue] = + engine.getting.deleteStorage(request) + def updateStorage( + request: UpdateStorageRequest[StorageKey, StorageValue] + ): UpdateStorageResponse[StorageKey, StorageValue] = + engine.getting.updateStorage(request) + def getStorage( + request: GetStorageRequest[StorageKey, StorageValue] + ): GetStorageResponse[StorageKey, StorageValue] = + engine.getting.getStorage(request) + def findStorages( + request: FindStoragesRequest[StorageKey, StorageValue] + ): FindStoragesResponse[StorageKey, StorageValue] = + engine.getting.findStorages(request) } } diff --git a/src/main/scala/ru/otus/sc/Engine.scala b/src/main/scala/ru/otus/sc/Engine.scala new file mode 100644 index 0000000..6cde543 --- /dev/null +++ b/src/main/scala/ru/otus/sc/Engine.scala @@ -0,0 +1,80 @@ +package ru.otus.sc + +import ru.otus.sc.Engine.{Greeted, StorageKey, StorageValue} +import ru.otus.sc.countdown.dao.impl.CountdownDaoImpl +import ru.otus.sc.countdown.service.CountdownService +import ru.otus.sc.countdown.service.impl.CountdownServiceImpl +import ru.otus.sc.counter.dao.impl.CounterDaoImpl +import ru.otus.sc.counter.service.CounterService +import ru.otus.sc.counter.service.impl.CounterServiceImpl +import ru.otus.sc.echo.dao.impl.EchoDaoImpl +import ru.otus.sc.echo.service.EchoService +import ru.otus.sc.echo.service.impl.EchoServiceImpl +import ru.otus.sc.greet.dao.impl.GreetingDaoImpl +import ru.otus.sc.greet.service.GreetingService +import ru.otus.sc.greet.service.impl.GreetingServiceImpl +import ru.otus.sc.reverse.dao.impl.ReverseDaoImpl +import ru.otus.sc.reverse.service.ReverseService +import ru.otus.sc.reverse.service.impl.ReverseServiceImpl +import ru.otus.sc.storage.dao.impl.StorageDaoImpl +import ru.otus.sc.storage.service.StorageService +import ru.otus.sc.storage.service.impl.StorageServiceImpl +import ru.otus.sc.sum.dao.impl.SumDaoImpl +import ru.otus.sc.sum.service.SumService +import ru.otus.sc.sum.service.impl.SumServiceImpl +import ru.otus.sc.user.dao.impl.{UserDaoImpl, UserTagDaoImpl} +import ru.otus.sc.user.model.User +import ru.otus.sc.user.service.impl.{UserServiceImpl, UserTagServiceImpl} +import ru.otus.sc.user.service.{UserService, UserTagService} + +// Helper class which aggregates service entries to single point +case class Engine( + countdowning: CountdownService, + counting: CounterService, + echoing: EchoService, + getting: StorageService[StorageKey, StorageValue], + greeting: GreetingService[Greeted], + reversing: ReverseService, + summing: SumService, + usering: UserService, + userTagging: UserTagService +) + +object Engine { + type StorageKey = String + type StorageValue = String + type Greeted = User + + def apply(): Engine = { + val CountdownDao = new CountdownDaoImpl + val countdownService = new CountdownServiceImpl(CountdownDao) + val CounterDao = new CounterDaoImpl + val counterService = new CounterServiceImpl(CounterDao) + val echoDao = new EchoDaoImpl + val echoService = new EchoServiceImpl(echoDao) + val greetingDao = new GreetingDaoImpl + val greetingService = new GreetingServiceImpl[Greeted](greetingDao) + val storageDao = new StorageDaoImpl[StorageKey, StorageValue] + val storageService = new StorageServiceImpl[StorageKey, StorageValue](storageDao) + val reverseDao = new ReverseDaoImpl + val reverseService = new ReverseServiceImpl(reverseDao) + val sumDao = new SumDaoImpl + val sumService = new SumServiceImpl(sumDao) + val userDao = new UserDaoImpl + val userService = new UserServiceImpl(userDao) + val userTagDao = new UserTagDaoImpl + val userTagService = new UserTagServiceImpl(userTagDao) + + Engine( + countdownService, + counterService, + echoService, + storageService, + greetingService, + reverseService, + sumService, + userService, + userTagService + ) + } +} diff --git a/src/main/scala/ru/otus/sc/countdown/dao/CountdownDao.scala b/src/main/scala/ru/otus/sc/countdown/dao/CountdownDao.scala new file mode 100644 index 0000000..9da76a3 --- /dev/null +++ b/src/main/scala/ru/otus/sc/countdown/dao/CountdownDao.scala @@ -0,0 +1,15 @@ +package ru.otus.sc.countdown.dao + +import ru.otus.sc.countdown.model.Countdown +import ru.otus.sc.countdown.model.Countdown.{CompareValues, CountdownId, CountdownValue, UpdaterId} + +trait CountdownDao { + def createCountdown(countdown: Countdown): Countdown + def deleteCountdown(id: CountdownId): Option[Countdown] + def getCountdown(id: CountdownId): Option[Countdown] + def updateCountdown(countdown: Countdown): Option[Countdown] + def findCountdownsByValue(value: CountdownValue, predicate: CompareValues): Seq[Countdown] + def findCountdownsByUpdater(updater: UpdaterId): Seq[Countdown] + def findAllDone: Seq[Countdown] + def findAllNonDone: Seq[Countdown] +} diff --git a/src/main/scala/ru/otus/sc/countdown/dao/impl/CountdownDaoImpl.scala b/src/main/scala/ru/otus/sc/countdown/dao/impl/CountdownDaoImpl.scala new file mode 100644 index 0000000..697167e --- /dev/null +++ b/src/main/scala/ru/otus/sc/countdown/dao/impl/CountdownDaoImpl.scala @@ -0,0 +1,72 @@ +package ru.otus.sc.countdown.dao.impl + +import java.util.UUID + +import ru.otus.sc.countdown.dao.CountdownDao +import ru.otus.sc.countdown.model.Countdown +import ru.otus.sc.countdown.model.Countdown.{CompareValues, CountdownId, CountdownValue, UpdaterId} + +class CountdownDaoImpl extends CountdownDao { + private var countdowns = Map[CountdownId, Countdown]() + + def createCountdown(countdown: Countdown): Countdown = { + + val id = UUID.randomUUID() + val newCountdown = countdown match { + case Countdown.Done(_) => Countdown.Done(Some(id)) + case Countdown.Tick(_, updater, value) => Countdown.Tick(Some(id), updater, value) + } + countdowns += (id -> newCountdown) + newCountdown + } + + def deleteCountdown(id: CountdownId): Option[Countdown] = + for { + deletedCountdown <- countdowns.get(id) + } yield { + countdowns -= id + deletedCountdown + } + + def getCountdown(id: CountdownId): Option[Countdown] = countdowns.get(id) + + def updateCountdown(countdown: Countdown): Option[Countdown] = { + // only CountdownTick can be updated + val (countdownId, updaterId) = countdown match { + case Countdown.Tick(id, updater, _) => (id, Some(updater)) + case Countdown.Done(_) => (None, None) + } + + for { + id <- countdownId + updater <- updaterId + currentCountdown <- countdowns.get(id) + } yield { + val newCountdown = currentCountdown match { + case alreadyDone @ Countdown.Done(_) => alreadyDone + case Countdown.Tick(id, _, value) => + val newValue = value - 1L + if (newValue <= 0) Countdown.Done(id) + else Countdown.Tick(id, updater, newValue) + } + countdowns += (id -> newCountdown) + newCountdown + } + } + + def findCountdownsByValue(value: CountdownValue, predicate: CompareValues): Seq[Countdown] = + countdowns.values.collect { + case x @ Countdown.Tick(_, _, currentValue) if predicate(value, currentValue) => x + }.toVector + + def findCountdownsByUpdater(updater: UpdaterId): Seq[Countdown] = + countdowns.values.collect { + case x @ Countdown.Tick(_, updaterId, _) if updaterId == updater => x + }.toVector + + def findAllDone: Seq[Countdown] = + countdowns.values.collect { case x @ Countdown.Done(_) => x }.toVector + + def findAllNonDone: Seq[Countdown] = + countdowns.values.collect { case x @ Countdown.Tick(_, _, _) => x }.toVector +} diff --git a/src/main/scala/ru/otus/sc/countdown/model/Countdown.scala b/src/main/scala/ru/otus/sc/countdown/model/Countdown.scala new file mode 100644 index 0000000..e2f7bf6 --- /dev/null +++ b/src/main/scala/ru/otus/sc/countdown/model/Countdown.scala @@ -0,0 +1,17 @@ +package ru.otus.sc.countdown.model + +import java.util.UUID + +sealed trait Countdown + +object Countdown { + type CountdownId = UUID + type UpdaterId = UUID + type CountdownValue = Long + type CompareValues = (CountdownValue, CountdownValue) => Boolean + + case class Tick(id: Option[CountdownId], updater: UpdaterId, value: CountdownValue = 1L) + extends Countdown + case class Done(id: Option[CountdownId]) extends Countdown + +} diff --git a/src/main/scala/ru/otus/sc/countdown/model/CreateCountdown.scala b/src/main/scala/ru/otus/sc/countdown/model/CreateCountdown.scala new file mode 100644 index 0000000..3b1b404 --- /dev/null +++ b/src/main/scala/ru/otus/sc/countdown/model/CreateCountdown.scala @@ -0,0 +1,9 @@ +package ru.otus.sc.countdown.model + +case class CreateCountdownRequest(countdown: Countdown) + +sealed trait CreateCountdownResponse +object CreateCountdownResponse { + case class Created(countdown: Countdown) extends CreateCountdownResponse + case class Error(error: String) extends CreateCountdownResponse +} diff --git a/src/main/scala/ru/otus/sc/countdown/model/DeleteCountdown.scala b/src/main/scala/ru/otus/sc/countdown/model/DeleteCountdown.scala new file mode 100644 index 0000000..59dd8e7 --- /dev/null +++ b/src/main/scala/ru/otus/sc/countdown/model/DeleteCountdown.scala @@ -0,0 +1,11 @@ +package ru.otus.sc.countdown.model + +import ru.otus.sc.countdown.model.Countdown.CountdownId + +case class DeleteCountdownRequest(id: CountdownId) + +sealed trait DeleteCountdownResponse +object DeleteCountdownResponse { + case class Deleted(countdown: Countdown) extends DeleteCountdownResponse + case class NotFound(id: CountdownId) extends DeleteCountdownResponse +} diff --git a/src/main/scala/ru/otus/sc/countdown/model/FindCountdown.scala b/src/main/scala/ru/otus/sc/countdown/model/FindCountdown.scala new file mode 100644 index 0000000..ab9bbfa --- /dev/null +++ b/src/main/scala/ru/otus/sc/countdown/model/FindCountdown.scala @@ -0,0 +1,16 @@ +package ru.otus.sc.countdown.model + +import ru.otus.sc.countdown.model.Countdown.{CompareValues, CountdownValue, UpdaterId} + +sealed trait FindCountdownsRequest +object FindCountdownsRequest { + case class ByValue(value: CountdownValue, predicate: CompareValues) extends FindCountdownsRequest + case class ByUpdater(updater: UpdaterId) extends FindCountdownsRequest + case class AllDone() extends FindCountdownsRequest + case class AllNonDone() extends FindCountdownsRequest +} + +sealed trait FindCountdownsResponse +object FindCountdownsResponse { + case class Result(countdowns: Seq[Countdown]) extends FindCountdownsResponse +} diff --git a/src/main/scala/ru/otus/sc/countdown/model/GetCountdown.scala b/src/main/scala/ru/otus/sc/countdown/model/GetCountdown.scala new file mode 100644 index 0000000..5d67825 --- /dev/null +++ b/src/main/scala/ru/otus/sc/countdown/model/GetCountdown.scala @@ -0,0 +1,11 @@ +package ru.otus.sc.countdown.model + +import ru.otus.sc.countdown.model.Countdown.CountdownId + +case class GetCountdownRequest(id: CountdownId) + +sealed trait GetCountdownResponse +object GetCountdownResponse { + case class Found(countdown: Countdown) extends GetCountdownResponse + case class NotFound(id: CountdownId) extends GetCountdownResponse +} diff --git a/src/main/scala/ru/otus/sc/countdown/model/UpdateCountdown.scala b/src/main/scala/ru/otus/sc/countdown/model/UpdateCountdown.scala new file mode 100644 index 0000000..078c378 --- /dev/null +++ b/src/main/scala/ru/otus/sc/countdown/model/UpdateCountdown.scala @@ -0,0 +1,13 @@ +package ru.otus.sc.countdown.model + +import ru.otus.sc.countdown.model.Countdown.CountdownId + +case class UpdateCountdownRequest(countdown: Countdown) + +sealed trait UpdateCountdownResponse +object UpdateCountdownResponse { + case class Updated(countdown: Countdown) extends UpdateCountdownResponse + case class NotFound(id: CountdownId) extends UpdateCountdownResponse + case object CanNotUpdateDone extends UpdateCountdownResponse + case object ErrorWrongId extends UpdateCountdownResponse +} diff --git a/src/main/scala/ru/otus/sc/countdown/service/CountdownService.scala b/src/main/scala/ru/otus/sc/countdown/service/CountdownService.scala new file mode 100644 index 0000000..8eb73a8 --- /dev/null +++ b/src/main/scala/ru/otus/sc/countdown/service/CountdownService.scala @@ -0,0 +1,11 @@ +package ru.otus.sc.countdown.service + +import ru.otus.sc.countdown.model._ + +trait CountdownService { + def createCountdown(request: CreateCountdownRequest): CreateCountdownResponse + def deleteCountdown(request: DeleteCountdownRequest): DeleteCountdownResponse + def updateCountdown(request: UpdateCountdownRequest): UpdateCountdownResponse + def getCountdown(request: GetCountdownRequest): GetCountdownResponse + def findCountdowns(request: FindCountdownsRequest): FindCountdownsResponse +} diff --git a/src/main/scala/ru/otus/sc/countdown/service/impl/CountdownServiceImpl.scala b/src/main/scala/ru/otus/sc/countdown/service/impl/CountdownServiceImpl.scala new file mode 100644 index 0000000..b150dbf --- /dev/null +++ b/src/main/scala/ru/otus/sc/countdown/service/impl/CountdownServiceImpl.scala @@ -0,0 +1,58 @@ +package ru.otus.sc.countdown.service.impl + +import ru.otus.sc.countdown.dao.CountdownDao +import ru.otus.sc.countdown.model.{Countdown, CreateCountdownResponse, _} +import ru.otus.sc.countdown.service.CountdownService + +class CountdownServiceImpl(dao: CountdownDao) extends CountdownService { + def createCountdown(request: CreateCountdownRequest): CreateCountdownResponse = + request.countdown match { + case countdown @ Countdown.Done(_) => + CreateCountdownResponse.Created(dao.createCountdown(countdown)) + case countdown @ Countdown.Tick(_, _, value) => + // it can return CreateCountdownResponse.Error + // but in current CountdownDao implementation there is not option for this + // so emulate it with countdown.value set less than 1 + if (value < 1L) + CreateCountdownResponse.Error("Countdown initial value has to be more or equal 1") + else + CreateCountdownResponse.Created(dao.createCountdown(countdown)) + } + def deleteCountdown(request: DeleteCountdownRequest): DeleteCountdownResponse = + dao + .deleteCountdown(request.id) + .map(DeleteCountdownResponse.Deleted) + .getOrElse(DeleteCountdownResponse.NotFound(request.id)) + + def updateCountdown(request: UpdateCountdownRequest): UpdateCountdownResponse = + request.countdown match { + // Done can not be updated anymore + case Countdown.Done(_) => UpdateCountdownResponse.CanNotUpdateDone + case countdown @ Countdown.Tick(Some(id), _, _) => + dao + .updateCountdown(countdown) + .map(UpdateCountdownResponse.Updated) + .getOrElse(UpdateCountdownResponse.NotFound(id)) + case Countdown.Tick(None, _, _) => UpdateCountdownResponse.ErrorWrongId + } + + def getCountdown(request: GetCountdownRequest): GetCountdownResponse = + dao + .getCountdown(request.id) + .map(GetCountdownResponse.Found) + .getOrElse(GetCountdownResponse.NotFound(request.id)) + + def findCountdowns(request: FindCountdownsRequest): FindCountdownsResponse = { + val found = request match { + case FindCountdownsRequest.ByValue(value, predicate) => + dao.findCountdownsByValue(value, predicate) + case FindCountdownsRequest.ByUpdater(updater) => + dao.findCountdownsByUpdater(updater) + case FindCountdownsRequest.AllDone() => + dao.findAllDone + case FindCountdownsRequest.AllNonDone() => + dao.findAllNonDone + } + FindCountdownsResponse.Result(found) + } +} diff --git a/src/main/scala/ru/otus/sc/counter/dao/CounterDao.scala b/src/main/scala/ru/otus/sc/counter/dao/CounterDao.scala new file mode 100644 index 0000000..422782e --- /dev/null +++ b/src/main/scala/ru/otus/sc/counter/dao/CounterDao.scala @@ -0,0 +1,14 @@ +package ru.otus.sc.counter.dao + +import ru.otus.sc.counter.model.Counter +import ru.otus.sc.counter.model.Counter.{CounterId, CounterTime, CounterValue} +import ru.otus.sc.counter.model.FindCountersRequest.{CompareTimes, CompareValues} + +trait CounterDao { + def createCounter(counter: Counter): Counter + def deleteCounter(id: CounterId): Option[Counter] + def getCounter(id: CounterId): Option[Counter] + def updateCounter(id: CounterId): Option[Counter] + def findCountersByValue(value: CounterValue, predicate: CompareValues): Seq[Counter] + def findCountersByTimestamp(timestamp: CounterTime, predicate: CompareTimes): Seq[Counter] +} diff --git a/src/main/scala/ru/otus/sc/counter/dao/impl/CounterDaoImpl.scala b/src/main/scala/ru/otus/sc/counter/dao/impl/CounterDaoImpl.scala new file mode 100644 index 0000000..7fa7ff6 --- /dev/null +++ b/src/main/scala/ru/otus/sc/counter/dao/impl/CounterDaoImpl.scala @@ -0,0 +1,51 @@ +package ru.otus.sc.counter.dao.impl + +import java.time.LocalDateTime +import java.util.UUID + +import ru.otus.sc.counter.dao.CounterDao +import ru.otus.sc.counter.model.Counter +import ru.otus.sc.counter.model.Counter.{CounterId, CounterTime, CounterValue} +import ru.otus.sc.counter.model.FindCountersRequest.{CompareTimes, CompareValues} + +class CounterDaoImpl extends CounterDao { + private var counters = Map[CounterId, Counter]() + + def createCounter(counter: Counter): Counter = { + val newId = UUID.randomUUID() + val newTime = LocalDateTime.now() + val newCounter = counter.copy(id = Some(newId), timestamp = Some(newTime)) + counters += (newId -> newCounter) + newCounter + } + + def deleteCounter(id: CounterId): Option[Counter] = + counters.get(id) match { + case deletedCounter @ Some(_) => + counters -= id + deletedCounter + case None => None + } + + def getCounter(id: CounterId): Option[Counter] = counters.get(id) + + def updateCounter(id: CounterId): Option[Counter] = + counters.get(id) match { + case Some(counter) => + val newTime = LocalDateTime.now() + val newCounter = counter.copy(timestamp = Some(newTime), value = counter.value + 1L) + counters += (id -> newCounter) + Some(newCounter) + case None => None + } + + def findCountersByValue(value: CounterValue, predicate: CompareValues): Seq[Counter] = + counters.values.filter(counter => predicate(value, counter.value)).toVector + + def findCountersByTimestamp(timestamp: CounterTime, predicate: CompareTimes): Seq[Counter] = + counters.values.collect { counter => + counter.timestamp match { + case Some(time) if predicate(timestamp, time) => counter + } + }.toVector +} diff --git a/src/main/scala/ru/otus/sc/counter/model/Counter.scala b/src/main/scala/ru/otus/sc/counter/model/Counter.scala new file mode 100644 index 0000000..e07d44e --- /dev/null +++ b/src/main/scala/ru/otus/sc/counter/model/Counter.scala @@ -0,0 +1,13 @@ +package ru.otus.sc.counter.model + +import java.time.LocalDateTime +import java.util.UUID + +import ru.otus.sc.counter.model.Counter.{CounterId, CounterTime, CounterValue} + +case class Counter(id: Option[CounterId], timestamp: Option[CounterTime], value: CounterValue = 1L) +object Counter { + type CounterId = UUID + type CounterValue = Long + type CounterTime = LocalDateTime +} diff --git a/src/main/scala/ru/otus/sc/counter/model/CreateCounter.scala b/src/main/scala/ru/otus/sc/counter/model/CreateCounter.scala new file mode 100644 index 0000000..220709f --- /dev/null +++ b/src/main/scala/ru/otus/sc/counter/model/CreateCounter.scala @@ -0,0 +1,9 @@ +package ru.otus.sc.counter.model + +case class CreateCounterRequest(counter: Counter) + +sealed trait CreateCounterResponse +object CreateCounterResponse { + case class Created(counter: Counter) extends CreateCounterResponse + case class Error(error: String) extends CreateCounterResponse +} diff --git a/src/main/scala/ru/otus/sc/counter/model/DeleteCounter.scala b/src/main/scala/ru/otus/sc/counter/model/DeleteCounter.scala new file mode 100644 index 0000000..e9d8997 --- /dev/null +++ b/src/main/scala/ru/otus/sc/counter/model/DeleteCounter.scala @@ -0,0 +1,11 @@ +package ru.otus.sc.counter.model + +import ru.otus.sc.counter.model.Counter.CounterId + +case class DeleteCounterRequest(id: CounterId) + +sealed trait DeleteCounterResponse +object DeleteCounterResponse { + case class Deleted(counter: Counter) extends DeleteCounterResponse + case class NotFound(id: CounterId) extends DeleteCounterResponse +} diff --git a/src/main/scala/ru/otus/sc/counter/model/FindCounters.scala b/src/main/scala/ru/otus/sc/counter/model/FindCounters.scala new file mode 100644 index 0000000..193a6ff --- /dev/null +++ b/src/main/scala/ru/otus/sc/counter/model/FindCounters.scala @@ -0,0 +1,17 @@ +package ru.otus.sc.counter.model + +import ru.otus.sc.counter.model.Counter.{CounterTime, CounterValue} + +sealed trait FindCountersRequest +object FindCountersRequest { + type CompareValues = (CounterValue, CounterValue) => Boolean + type CompareTimes = (CounterTime, CounterTime) => Boolean + case class ByValue(value: CounterValue, predicate: CompareValues) extends FindCountersRequest + case class ByTimestamp(timestamp: CounterTime, predicate: CompareTimes) + extends FindCountersRequest +} + +sealed trait FindCountersResponse +object FindCountersResponse { + case class Result(counters: Seq[Counter]) extends FindCountersResponse +} diff --git a/src/main/scala/ru/otus/sc/counter/model/GetCounter.scala b/src/main/scala/ru/otus/sc/counter/model/GetCounter.scala new file mode 100644 index 0000000..608c58a --- /dev/null +++ b/src/main/scala/ru/otus/sc/counter/model/GetCounter.scala @@ -0,0 +1,11 @@ +package ru.otus.sc.counter.model + +import ru.otus.sc.counter.model.Counter.CounterId + +case class GetCounterRequest(id: CounterId) + +sealed trait GetCounterResponse +object GetCounterResponse { + case class Found(counter: Counter) extends GetCounterResponse + case class NotFound(id: CounterId) extends GetCounterResponse +} diff --git a/src/main/scala/ru/otus/sc/counter/model/UpdateCounter.scala b/src/main/scala/ru/otus/sc/counter/model/UpdateCounter.scala new file mode 100644 index 0000000..6afd138 --- /dev/null +++ b/src/main/scala/ru/otus/sc/counter/model/UpdateCounter.scala @@ -0,0 +1,11 @@ +package ru.otus.sc.counter.model + +import ru.otus.sc.counter.model.Counter.CounterId + +case class UpdateCounterRequest(id: CounterId) + +sealed trait UpdateCounterResponse +object UpdateCounterResponse { + case class Updated(counter: Counter) extends UpdateCounterResponse + case class NotFound(id: CounterId) extends UpdateCounterResponse +} diff --git a/src/main/scala/ru/otus/sc/counter/service/CounterService.scala b/src/main/scala/ru/otus/sc/counter/service/CounterService.scala new file mode 100644 index 0000000..e41bed5 --- /dev/null +++ b/src/main/scala/ru/otus/sc/counter/service/CounterService.scala @@ -0,0 +1,11 @@ +package ru.otus.sc.counter.service + +import ru.otus.sc.counter.model._ + +trait CounterService { + def createCounter(request: CreateCounterRequest): CreateCounterResponse + def deleteCounter(request: DeleteCounterRequest): DeleteCounterResponse + def updateCounter(request: UpdateCounterRequest): UpdateCounterResponse + def getCounter(request: GetCounterRequest): GetCounterResponse + def findCounters(request: FindCountersRequest): FindCountersResponse +} diff --git a/src/main/scala/ru/otus/sc/counter/service/impl/CounterServiceImpl.scala b/src/main/scala/ru/otus/sc/counter/service/impl/CounterServiceImpl.scala new file mode 100644 index 0000000..af6630a --- /dev/null +++ b/src/main/scala/ru/otus/sc/counter/service/impl/CounterServiceImpl.scala @@ -0,0 +1,43 @@ +package ru.otus.sc.counter.service.impl + +import ru.otus.sc.counter.dao.CounterDao +import ru.otus.sc.counter.model._ +import ru.otus.sc.counter.service.CounterService + +class CounterServiceImpl(dao: CounterDao) extends CounterService { + def createCounter(request: CreateCounterRequest): CreateCounterResponse = { + // it can return CreateCounterResponse.Error + // but in current CounterDao implementation there is not option for this + // so emulate it with counter.value set less than 1 + if (request.counter.value < 1L) + CreateCounterResponse.Error("Counter initial value has to be more or equal 1") + CreateCounterResponse.Created(dao.createCounter(request.counter)) + } + def deleteCounter(request: DeleteCounterRequest): DeleteCounterResponse = + dao + .deleteCounter(request.id) + .map(DeleteCounterResponse.Deleted) + .getOrElse(DeleteCounterResponse.NotFound(request.id)) + + def updateCounter(request: UpdateCounterRequest): UpdateCounterResponse = + dao + .updateCounter(request.id) + .map(UpdateCounterResponse.Updated) + .getOrElse(UpdateCounterResponse.NotFound(request.id)) + + def getCounter(request: GetCounterRequest): GetCounterResponse = + dao + .getCounter(request.id) + .map(GetCounterResponse.Found) + .getOrElse(GetCounterResponse.NotFound(request.id)) + + def findCounters(request: FindCountersRequest): FindCountersResponse = { + val found = request match { + case FindCountersRequest.ByValue(value, predicate) => + dao.findCountersByValue(value, predicate) + case FindCountersRequest.ByTimestamp(value, predicate) => + dao.findCountersByTimestamp(value, predicate) + } + FindCountersResponse.Result(found) + } +} diff --git a/src/main/scala/ru/otus/sc/echo/dao/EchoDao.scala b/src/main/scala/ru/otus/sc/echo/dao/EchoDao.scala new file mode 100644 index 0000000..a7b8db2 --- /dev/null +++ b/src/main/scala/ru/otus/sc/echo/dao/EchoDao.scala @@ -0,0 +1,6 @@ +package ru.otus.sc.echo.dao + +trait EchoDao { + def echoPrefix: String + def getResponse(request: String, repeatNum: Int): String +} diff --git a/src/main/scala/ru/otus/sc/echo/dao/impl/EchoDaoImpl.scala b/src/main/scala/ru/otus/sc/echo/dao/impl/EchoDaoImpl.scala new file mode 100644 index 0000000..ccfdc9e --- /dev/null +++ b/src/main/scala/ru/otus/sc/echo/dao/impl/EchoDaoImpl.scala @@ -0,0 +1,12 @@ +package ru.otus.sc.echo.dao.impl + +import ru.otus.sc.echo.dao.EchoDao + +class EchoDaoImpl extends EchoDao { + val echoPrefix: String = "Echo reply: " + + def getResponse(request: String, repeatNum: Int): String = { + // use collection for making request + List.fill(repeatNum)(request).mkString(echoPrefix, " ", "") + } +} diff --git a/src/main/scala/ru/otus/sc/echo/model/EchoRequest.scala b/src/main/scala/ru/otus/sc/echo/model/EchoRequest.scala new file mode 100644 index 0000000..d1bbccd --- /dev/null +++ b/src/main/scala/ru/otus/sc/echo/model/EchoRequest.scala @@ -0,0 +1,3 @@ +package ru.otus.sc.echo.model + +case class EchoRequest(echoRequest: String, repeatNum: Int = 1) diff --git a/src/main/scala/ru/otus/sc/echo/model/EchoResponse.scala b/src/main/scala/ru/otus/sc/echo/model/EchoResponse.scala new file mode 100644 index 0000000..0d36726 --- /dev/null +++ b/src/main/scala/ru/otus/sc/echo/model/EchoResponse.scala @@ -0,0 +1,7 @@ +package ru.otus.sc.echo.model + +sealed trait EchoResponse +object EchoResponse { + case class Answer(answer: String) extends EchoResponse + case class Error(error: String) extends EchoResponse +} diff --git a/src/main/scala/ru/otus/sc/echo/service/EchoService.scala b/src/main/scala/ru/otus/sc/echo/service/EchoService.scala new file mode 100644 index 0000000..2232913 --- /dev/null +++ b/src/main/scala/ru/otus/sc/echo/service/EchoService.scala @@ -0,0 +1,7 @@ +package ru.otus.sc.echo.service + +import ru.otus.sc.echo.model.{EchoRequest, EchoResponse} + +trait EchoService { + def echo(request: EchoRequest): EchoResponse +} diff --git a/src/main/scala/ru/otus/sc/echo/service/impl/EchoServiceImpl.scala b/src/main/scala/ru/otus/sc/echo/service/impl/EchoServiceImpl.scala new file mode 100644 index 0000000..e1607e8 --- /dev/null +++ b/src/main/scala/ru/otus/sc/echo/service/impl/EchoServiceImpl.scala @@ -0,0 +1,13 @@ +package ru.otus.sc.echo.service.impl + +import ru.otus.sc.echo.dao.EchoDao +import ru.otus.sc.echo.model.{EchoRequest, EchoResponse} +import ru.otus.sc.echo.service.EchoService + +class EchoServiceImpl(dao: EchoDao) extends EchoService { + + def echo(request: EchoRequest): EchoResponse = + if (request.repeatNum > 0 & request.repeatNum <= 5) + EchoResponse.Answer(s"${dao.getResponse(request.echoRequest, request.repeatNum)}") + else EchoResponse.Error("Number of replies has to be from 1 to 5") +} diff --git a/src/main/scala/ru/otus/sc/greet/model/GreetRequest.scala b/src/main/scala/ru/otus/sc/greet/model/GreetRequest.scala index 3115535..83f7f4e 100644 --- a/src/main/scala/ru/otus/sc/greet/model/GreetRequest.scala +++ b/src/main/scala/ru/otus/sc/greet/model/GreetRequest.scala @@ -1,3 +1,5 @@ package ru.otus.sc.greet.model -case class GreetRequest(name: String, isHuman: Boolean = true) +case class GreetRequest[T](name: T, isHuman: Boolean = true)(implicit conv: T => String) { + def extractName(): String = conv(name) +} diff --git a/src/main/scala/ru/otus/sc/greet/service/GreetingService.scala b/src/main/scala/ru/otus/sc/greet/service/GreetingService.scala index 2975165..675f4ac 100644 --- a/src/main/scala/ru/otus/sc/greet/service/GreetingService.scala +++ b/src/main/scala/ru/otus/sc/greet/service/GreetingService.scala @@ -2,6 +2,6 @@ package ru.otus.sc.greet.service import ru.otus.sc.greet.model.{GreetRequest, GreetResponse} -trait GreetingService { - def greet(request: GreetRequest): GreetResponse +trait GreetingService[T] { + def greet(request: GreetRequest[T]): GreetResponse } diff --git a/src/main/scala/ru/otus/sc/greet/service/impl/GreetingServiceImpl.scala b/src/main/scala/ru/otus/sc/greet/service/impl/GreetingServiceImpl.scala index fe63a3c..f39c540 100644 --- a/src/main/scala/ru/otus/sc/greet/service/impl/GreetingServiceImpl.scala +++ b/src/main/scala/ru/otus/sc/greet/service/impl/GreetingServiceImpl.scala @@ -4,9 +4,9 @@ import ru.otus.sc.greet.dao.GreetingDao import ru.otus.sc.greet.model.{GreetRequest, GreetResponse} import ru.otus.sc.greet.service.GreetingService -class GreetingServiceImpl(dao: GreetingDao) extends GreetingService { - def greet(request: GreetRequest): GreetResponse = +class GreetingServiceImpl[T](dao: GreetingDao) extends GreetingService[T] { + def greet(request: GreetRequest[T]): GreetResponse = if (request.isHuman) - GreetResponse(s"${dao.greetingPrefix} ${request.name} ${dao.greetingPostfix}") + GreetResponse(s"${dao.greetingPrefix} ${request.extractName()} ${dao.greetingPostfix}") else GreetResponse("AAAAAAAAAA!!!!!!") } diff --git a/src/main/scala/ru/otus/sc/reverse/dao/ReverseDao.scala b/src/main/scala/ru/otus/sc/reverse/dao/ReverseDao.scala new file mode 100644 index 0000000..f3ff8bb --- /dev/null +++ b/src/main/scala/ru/otus/sc/reverse/dao/ReverseDao.scala @@ -0,0 +1,5 @@ +package ru.otus.sc.reverse.dao + +trait ReverseDao { + def reverse(word: String): String +} diff --git a/src/main/scala/ru/otus/sc/reverse/dao/impl/ReverseDaoImpl.scala b/src/main/scala/ru/otus/sc/reverse/dao/impl/ReverseDaoImpl.scala new file mode 100644 index 0000000..f5d479a --- /dev/null +++ b/src/main/scala/ru/otus/sc/reverse/dao/impl/ReverseDaoImpl.scala @@ -0,0 +1,8 @@ +package ru.otus.sc.reverse.dao.impl + +import ru.otus.sc.reverse.dao.ReverseDao + +class ReverseDaoImpl extends ReverseDao { + // use string collection method + def reverse(word: String): String = word.reverse +} diff --git a/src/main/scala/ru/otus/sc/reverse/model/ReverseRequest.scala b/src/main/scala/ru/otus/sc/reverse/model/ReverseRequest.scala new file mode 100644 index 0000000..d8b4625 --- /dev/null +++ b/src/main/scala/ru/otus/sc/reverse/model/ReverseRequest.scala @@ -0,0 +1,3 @@ +package ru.otus.sc.reverse.model + +case class ReverseRequest(requestedWord: String) diff --git a/src/main/scala/ru/otus/sc/reverse/model/ReverseResponse.scala b/src/main/scala/ru/otus/sc/reverse/model/ReverseResponse.scala new file mode 100644 index 0000000..64bc6cf --- /dev/null +++ b/src/main/scala/ru/otus/sc/reverse/model/ReverseResponse.scala @@ -0,0 +1,3 @@ +package ru.otus.sc.reverse.model + +case class ReverseResponse(reversedWord: String) diff --git a/src/main/scala/ru/otus/sc/reverse/service/ReverseService.scala b/src/main/scala/ru/otus/sc/reverse/service/ReverseService.scala new file mode 100644 index 0000000..3b71d14 --- /dev/null +++ b/src/main/scala/ru/otus/sc/reverse/service/ReverseService.scala @@ -0,0 +1,7 @@ +package ru.otus.sc.reverse.service + +import ru.otus.sc.reverse.model.{ReverseRequest, ReverseResponse} + +trait ReverseService { + def reverse(request: ReverseRequest): ReverseResponse +} diff --git a/src/main/scala/ru/otus/sc/reverse/service/impl/ReverseServiceImpl.scala b/src/main/scala/ru/otus/sc/reverse/service/impl/ReverseServiceImpl.scala new file mode 100644 index 0000000..91712b2 --- /dev/null +++ b/src/main/scala/ru/otus/sc/reverse/service/impl/ReverseServiceImpl.scala @@ -0,0 +1,12 @@ +package ru.otus.sc.reverse.service.impl + +import ru.otus.sc.reverse.dao.ReverseDao +import ru.otus.sc.reverse.model.{ReverseRequest, ReverseResponse} +import ru.otus.sc.reverse.service.ReverseService + +class ReverseServiceImpl(dao: ReverseDao) extends ReverseService { + + def reverse(request: ReverseRequest): ReverseResponse = { + ReverseResponse(dao.reverse(request.requestedWord)) + } +} diff --git a/src/main/scala/ru/otus/sc/storage/dao/StorageDao.scala b/src/main/scala/ru/otus/sc/storage/dao/StorageDao.scala new file mode 100644 index 0000000..d0cf3e3 --- /dev/null +++ b/src/main/scala/ru/otus/sc/storage/dao/StorageDao.scala @@ -0,0 +1,12 @@ +package ru.otus.sc.storage.dao + +import ru.otus.sc.storage.model.StorageEntry + +trait StorageDao[K, V] { + def createStorage(entry: StorageEntry[K, V]): Option[StorageEntry[K, V]] + def getStorage(key: K): Option[StorageEntry[K, V]] + def deleteStorage(key: K): Option[StorageEntry[K, V]] + def updateStorage(entry: StorageEntry[K, V]): Option[StorageEntry[K, V]] + def findByValue(value: V): Seq[StorageEntry[K, V]] + def findValuesByPredicate(predicate: V => Boolean): Seq[StorageEntry[K, V]] +} diff --git a/src/main/scala/ru/otus/sc/storage/dao/impl/StorageDaoImpl.scala b/src/main/scala/ru/otus/sc/storage/dao/impl/StorageDaoImpl.scala new file mode 100644 index 0000000..7ab3c53 --- /dev/null +++ b/src/main/scala/ru/otus/sc/storage/dao/impl/StorageDaoImpl.scala @@ -0,0 +1,45 @@ +package ru.otus.sc.storage.dao.impl + +import ru.otus.sc.storage.dao.StorageDao +import ru.otus.sc.storage.model.StorageEntry + +class StorageDaoImpl[K, V] extends StorageDao[K, V] { + private var storages: Map[K, V] = Map.empty + + def createStorage(entry: StorageEntry[K, V]): Option[StorageEntry[K, V]] = + storages.get(entry.key) match { + case Some(_) => None + case None => + storages += (entry.key -> entry.value) + Some(entry) + } + + def getStorage(key: K): Option[StorageEntry[K, V]] = + storages.get(key).map(value => StorageEntry(key, value)) + + def deleteStorage(key: K): Option[StorageEntry[K, V]] = + for { + value <- storages.get(key) + } yield { + storages -= key + StorageEntry(key, value) + } + + def updateStorage(entry: StorageEntry[K, V]): Option[StorageEntry[K, V]] = + for { + _ <- storages.get(entry.key) + } yield { + storages += (entry.key -> entry.value) + entry + } + + def findByValue(value: V): Seq[StorageEntry[K, V]] = + storages.collect { + case (k, v) if v == value => StorageEntry[K, V](k, v) + }.toVector + + def findValuesByPredicate(predicate: V => Boolean): Seq[StorageEntry[K, V]] = + storages.collect { + case (k, v) if predicate(v) => StorageEntry[K, V](k, v) + }.toVector +} diff --git a/src/main/scala/ru/otus/sc/storage/model/CreateStorage.scala b/src/main/scala/ru/otus/sc/storage/model/CreateStorage.scala new file mode 100644 index 0000000..e010e82 --- /dev/null +++ b/src/main/scala/ru/otus/sc/storage/model/CreateStorage.scala @@ -0,0 +1,9 @@ +package ru.otus.sc.storage.model + +case class CreateStorageRequest[K, V](entry: StorageEntry[K, V]) + +trait CreateStorageResponse[K, V] +object CreateStorageResponse { + case class Created[K, V](entry: StorageEntry[K, V]) extends CreateStorageResponse[K, V] + case class ErrorKeyExists[K, V](key: K) extends CreateStorageResponse[K, V] +} diff --git a/src/main/scala/ru/otus/sc/storage/model/DeleteStorage.scala b/src/main/scala/ru/otus/sc/storage/model/DeleteStorage.scala new file mode 100644 index 0000000..3b93119 --- /dev/null +++ b/src/main/scala/ru/otus/sc/storage/model/DeleteStorage.scala @@ -0,0 +1,9 @@ +package ru.otus.sc.storage.model + +case class DeleteStorageRequest[K, _](key: K) + +trait DeleteStorageResponse[K, V] +object DeleteStorageResponse { + case class Deleted[K, V](entry: StorageEntry[K, V]) extends DeleteStorageResponse[K, V] + case class NotFound[K, V](key: K) extends DeleteStorageResponse[K, V] +} diff --git a/src/main/scala/ru/otus/sc/storage/model/FindStorages.scala b/src/main/scala/ru/otus/sc/storage/model/FindStorages.scala new file mode 100644 index 0000000..d556924 --- /dev/null +++ b/src/main/scala/ru/otus/sc/storage/model/FindStorages.scala @@ -0,0 +1,12 @@ +package ru.otus.sc.storage.model + +trait FindStoragesRequest[K, V] +object FindStoragesRequest { + case class ByValue[K, V](value: V) extends FindStoragesRequest[K, V] + case class ValuesByPredicate[K, V](predicate: V => Boolean) extends FindStoragesRequest[K, V] +} + +trait FindStoragesResponse[K, V] +object FindStoragesResponse { + case class Result[K, V](entries: Seq[StorageEntry[K, V]]) extends FindStoragesResponse[K, V] +} diff --git a/src/main/scala/ru/otus/sc/storage/model/GetStorage.scala b/src/main/scala/ru/otus/sc/storage/model/GetStorage.scala new file mode 100644 index 0000000..4dfbc11 --- /dev/null +++ b/src/main/scala/ru/otus/sc/storage/model/GetStorage.scala @@ -0,0 +1,9 @@ +package ru.otus.sc.storage.model + +case class GetStorageRequest[K, V](key: K) + +trait GetStorageResponse[K, V] +object GetStorageResponse { + case class Found[K, V](entry: StorageEntry[K, V]) extends GetStorageResponse[K, V] + case class NotFound[K, V](key: K) extends GetStorageResponse[K, V] +} diff --git a/src/main/scala/ru/otus/sc/storage/model/StorageEntry.scala b/src/main/scala/ru/otus/sc/storage/model/StorageEntry.scala new file mode 100644 index 0000000..52f8528 --- /dev/null +++ b/src/main/scala/ru/otus/sc/storage/model/StorageEntry.scala @@ -0,0 +1,3 @@ +package ru.otus.sc.storage.model + +case class StorageEntry[K, V](key: K, value: V) diff --git a/src/main/scala/ru/otus/sc/storage/model/UpdateStorage.scala b/src/main/scala/ru/otus/sc/storage/model/UpdateStorage.scala new file mode 100644 index 0000000..1072bd8 --- /dev/null +++ b/src/main/scala/ru/otus/sc/storage/model/UpdateStorage.scala @@ -0,0 +1,9 @@ +package ru.otus.sc.storage.model + +case class UpdateStorageRequest[K, V](entry: StorageEntry[K, V]) + +trait UpdateStorageResponse[K, V] +object UpdateStorageResponse { + case class Updated[K, V](entry: StorageEntry[K, V]) extends UpdateStorageResponse[K, V] + case class NotFound[K, V](key: K) extends UpdateStorageResponse[K, V] +} diff --git a/src/main/scala/ru/otus/sc/storage/service/StorageService.scala b/src/main/scala/ru/otus/sc/storage/service/StorageService.scala new file mode 100644 index 0000000..d33d3f1 --- /dev/null +++ b/src/main/scala/ru/otus/sc/storage/service/StorageService.scala @@ -0,0 +1,11 @@ +package ru.otus.sc.storage.service + +import ru.otus.sc.storage.model._ + +trait StorageService[K, V] { + def createStorage(request: CreateStorageRequest[K, V]): CreateStorageResponse[K, V] + def getStorage(request: GetStorageRequest[K, V]): GetStorageResponse[K, V] + def deleteStorage(request: DeleteStorageRequest[K, V]): DeleteStorageResponse[K, V] + def updateStorage(request: UpdateStorageRequest[K, V]): UpdateStorageResponse[K, V] + def findStorages(request: FindStoragesRequest[K, V]): FindStoragesResponse[K, V] +} diff --git a/src/main/scala/ru/otus/sc/storage/service/impl/StorageServiceImpl.scala b/src/main/scala/ru/otus/sc/storage/service/impl/StorageServiceImpl.scala new file mode 100644 index 0000000..61d5809 --- /dev/null +++ b/src/main/scala/ru/otus/sc/storage/service/impl/StorageServiceImpl.scala @@ -0,0 +1,39 @@ +package ru.otus.sc.storage.service.impl + +import ru.otus.sc.storage.dao.StorageDao +import ru.otus.sc.storage.model._ +import ru.otus.sc.storage.service.StorageService + +class StorageServiceImpl[K, V](dao: StorageDao[K, V]) extends StorageService[K, V] { + def createStorage(request: CreateStorageRequest[K, V]): CreateStorageResponse[K, V] = + dao + .createStorage(request.entry) + .map(CreateStorageResponse.Created[K, V]) + .getOrElse(CreateStorageResponse.ErrorKeyExists[K, V](request.entry.key)) + + def getStorage(request: GetStorageRequest[K, V]): GetStorageResponse[K, V] = + dao + .getStorage(request.key) + .map(GetStorageResponse.Found[K, V]) + .getOrElse(GetStorageResponse.NotFound[K, V](request.key)) + + def deleteStorage(request: DeleteStorageRequest[K, V]): DeleteStorageResponse[K, V] = + dao + .deleteStorage(request.key) + .map(DeleteStorageResponse.Deleted[K, V]) + .getOrElse(DeleteStorageResponse.NotFound[K, V](request.key)) + + def updateStorage(request: UpdateStorageRequest[K, V]): UpdateStorageResponse[K, V] = + dao + .updateStorage(request.entry) + .map(UpdateStorageResponse.Updated[K, V]) + .getOrElse(UpdateStorageResponse.NotFound[K, V](request.entry.key)) + + def findStorages(request: FindStoragesRequest[K, V]): FindStoragesResponse[K, V] = { + val found = request match { + case FindStoragesRequest.ByValue(value) => dao.findByValue(value) + case FindStoragesRequest.ValuesByPredicate(predicate) => dao.findValuesByPredicate(predicate) + } + FindStoragesResponse.Result[K, V](found) + } +} diff --git a/src/main/scala/ru/otus/sc/sum/dao/SumDao.scala b/src/main/scala/ru/otus/sc/sum/dao/SumDao.scala new file mode 100644 index 0000000..17059ab --- /dev/null +++ b/src/main/scala/ru/otus/sc/sum/dao/SumDao.scala @@ -0,0 +1,5 @@ +package ru.otus.sc.sum.dao + +trait SumDao { + def sum(a: Long, b: Long, external: Boolean): String +} diff --git a/src/main/scala/ru/otus/sc/sum/dao/impl/SumDaoImpl.scala b/src/main/scala/ru/otus/sc/sum/dao/impl/SumDaoImpl.scala new file mode 100644 index 0000000..fe85c66 --- /dev/null +++ b/src/main/scala/ru/otus/sc/sum/dao/impl/SumDaoImpl.scala @@ -0,0 +1,31 @@ +package ru.otus.sc.sum.dao.impl + +import ru.otus.sc.sum.dao.SumDao + +import scala.annotation.tailrec + +class SumDaoImpl extends SumDao { + def sum(a: Long, b: Long, external: Boolean): String = { + lazy val externalDelay = fib(10000000000L) + val result = { + if (!external) a + b + else { + externalDelay + 1 + a + b + } + } + + result.toString + } + + // Can not use Thread.sleep, so emulate delay with fibonacci 10000000000 calculation + private def fib(n: Long): Long = { + @tailrec + def fib_tail(n: Long, a: Long, b: Long): Long = { + if (n == 0) a + else fib_tail(n - 1L, b, a + b) + } + + fib_tail(n, 0L, 1L) + } +} diff --git a/src/main/scala/ru/otus/sc/sum/model/SumRequest.scala b/src/main/scala/ru/otus/sc/sum/model/SumRequest.scala new file mode 100644 index 0000000..0fe8c76 --- /dev/null +++ b/src/main/scala/ru/otus/sc/sum/model/SumRequest.scala @@ -0,0 +1,3 @@ +package ru.otus.sc.sum.model + +case class SumRequest(a: Long, b: Long, external: Boolean = false) diff --git a/src/main/scala/ru/otus/sc/sum/model/SumResponse.scala b/src/main/scala/ru/otus/sc/sum/model/SumResponse.scala new file mode 100644 index 0000000..08ce4e8 --- /dev/null +++ b/src/main/scala/ru/otus/sc/sum/model/SumResponse.scala @@ -0,0 +1,3 @@ +package ru.otus.sc.sum.model + +case class SumResponse(sumValue: String) diff --git a/src/main/scala/ru/otus/sc/sum/service/SumService.scala b/src/main/scala/ru/otus/sc/sum/service/SumService.scala new file mode 100644 index 0000000..52a34f5 --- /dev/null +++ b/src/main/scala/ru/otus/sc/sum/service/SumService.scala @@ -0,0 +1,7 @@ +package ru.otus.sc.sum.service + +import ru.otus.sc.sum.model.{SumRequest, SumResponse} + +trait SumService { + def sum(request: SumRequest): SumResponse +} diff --git a/src/main/scala/ru/otus/sc/sum/service/impl/SumServiceImpl.scala b/src/main/scala/ru/otus/sc/sum/service/impl/SumServiceImpl.scala new file mode 100644 index 0000000..aec2f0a --- /dev/null +++ b/src/main/scala/ru/otus/sc/sum/service/impl/SumServiceImpl.scala @@ -0,0 +1,12 @@ +package ru.otus.sc.sum.service.impl + +import ru.otus.sc.sum.dao.SumDao +import ru.otus.sc.sum.model.{SumRequest, SumResponse} +import ru.otus.sc.sum.service.SumService + +class SumServiceImpl(dao: SumDao) extends SumService { + + def sum(request: SumRequest): SumResponse = { + SumResponse(dao.sum(request.a, request.b, request.external)) + } +} diff --git a/src/main/scala/ru/otus/sc/user/dao/UserDao.scala b/src/main/scala/ru/otus/sc/user/dao/UserDao.scala new file mode 100644 index 0000000..48a3c30 --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/dao/UserDao.scala @@ -0,0 +1,16 @@ +package ru.otus.sc.user.dao + +import ru.otus.sc.user.model.User +import ru.otus.sc.user.model.User.UniqueUserId +import ru.otus.sc.user.model.UserTag.UserTagId + +trait UserDao { + def createUser(user: User): User + def getUser(uniqueUserId: UniqueUserId): Option[User] + def updateUser(user: User): Option[User] + def deleteUser(uniqueUserId: UniqueUserId): Option[User] + def findByFirstName(firstName: String): Seq[User] + def findByLastName(lastName: String): Seq[User] + def findByTag(tag: UserTagId): Seq[User] + def getAllUsers: Seq[User] +} diff --git a/src/main/scala/ru/otus/sc/user/dao/UserTagDao.scala b/src/main/scala/ru/otus/sc/user/dao/UserTagDao.scala new file mode 100644 index 0000000..606b9bc --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/dao/UserTagDao.scala @@ -0,0 +1,17 @@ +package ru.otus.sc.user.dao + +import ru.otus.sc.user.model.User.UniqueUserId +import ru.otus.sc.user.model.UserTag +import ru.otus.sc.user.model.UserTag.UserTagId + +trait UserTagDao { + def createTag(tag: UserTag): UserTag + def getTag(tagId: UserTagId): Option[UserTag] + def updateTag(tag: UserTag): Option[UserTag] + def deleteTag(tagId: UserTagId): Option[UserTag] + def findByName(tagName: String): Seq[UserTag] + def getUsersByTag(tagId: UserTagId): Seq[UniqueUserId] + def getAllTags: Seq[UserTag] + def tagUser(tagId: UserTagId, uniqueUserId: UniqueUserId): Option[(UserTagId, UniqueUserId)] + def untagUser(tagId: UserTagId, uniqueUserId: UniqueUserId): Option[(UserTagId, UniqueUserId)] +} diff --git a/src/main/scala/ru/otus/sc/user/dao/impl/UserDaoImpl.scala b/src/main/scala/ru/otus/sc/user/dao/impl/UserDaoImpl.scala new file mode 100644 index 0000000..d3165d0 --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/dao/impl/UserDaoImpl.scala @@ -0,0 +1,63 @@ +package ru.otus.sc.user.dao.impl + +import java.util.UUID + +import ru.otus.sc.user.dao.UserDao +import ru.otus.sc.user.model.User +import ru.otus.sc.user.model.User.UniqueUserId +import ru.otus.sc.user.model.UserTag.UserTagId + +class UserDaoImpl extends UserDao { + + private var users: Map[UniqueUserId, User] = Map.empty + // emulate pk for users + private var id: Long = 1 + + def createUser(user: User): User = { + val newUniqueId = UUID.randomUUID + val newUser = user.copy(id = Some(id), uniqueId = Some(newUniqueId)) + id += 1 + users += (newUniqueId -> newUser) + newUser + } + + def getUser(uniqueUserId: UniqueUserId): Option[User] = users.get(uniqueUserId) + + def updateUser(user: User): Option[User] = + for { + uniqueId <- user.uniqueId + existUser <- users.get(uniqueId) + // only uniqueId is required for user, id might not exist + // if id exists then it has to match existing user id + if user.id.isEmpty || existUser.id == user.id + } yield { + val newUser = if (user.id.isEmpty) user.copy(id = existUser.id) else user + users += (uniqueId -> newUser) + newUser + } + + def deleteUser(uniqueUserId: UniqueUserId): Option[User] = + users.get(uniqueUserId) match { + case deletedUser @ Some(_) => + users -= uniqueUserId + deletedUser + case _ => None + } + + def findByFirstName(firstName: String): Seq[User] = + users.values.filter(_.name.firstName == firstName).toVector + + def findByLastName(lastName: String): Seq[User] = + users.values + .collect(user => + user.name.lastName match { + case Some(userLastName) if userLastName == lastName => user + } + ) + .toVector + + def findByTag(tag: UserTagId): Seq[User] = + users.values.filter(_.tags.contains(tag)).toVector + + def getAllUsers: Seq[User] = users.values.toVector +} diff --git a/src/main/scala/ru/otus/sc/user/dao/impl/UserTagDaoImpl.scala b/src/main/scala/ru/otus/sc/user/dao/impl/UserTagDaoImpl.scala new file mode 100644 index 0000000..d1eb990 --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/dao/impl/UserTagDaoImpl.scala @@ -0,0 +1,72 @@ +package ru.otus.sc.user.dao.impl + +import ru.otus.sc.user.dao.UserTagDao +import ru.otus.sc.user.model.User.UniqueUserId +import ru.otus.sc.user.model.UserTag +import ru.otus.sc.user.model.UserTag.UserTagId + +class UserTagDaoImpl extends UserTagDao { + + private var tags: Map[UserTagId, UserTag] = Map.empty + // tag -> users mapping, due to tag does not keep associated users itself + private var taggedUsers: Map[UserTagId, Set[UniqueUserId]] = Map.empty + // emulate pk for tags + private var id: Long = 1 + + override def createTag(tag: UserTag): UserTag = { + val newUserTag = tag.copy(id = Some(id)) + tags += (id -> newUserTag) + taggedUsers += (id -> Set.empty) + id += 1 + newUserTag + } + + override def getTag(tagId: UserTagId): Option[UserTag] = tags.get(tagId) + + override def updateTag(tag: UserTag): Option[UserTag] = + for { + tagId <- tag.id + _ <- tags.get(tagId) + } yield { + tags += (tagId -> tag) + tag + } + + override def deleteTag(tagId: UserTagId): Option[UserTag] = + tags.get(tagId) match { + case deletedTag @ Some(_) => + tags -= tagId + deletedTag + case None => None + } + + override def findByName(tagName: String): Seq[UserTag] = + tags.values.filter(_.tagName == tagName).toVector + + override def getUsersByTag(tagId: UserTagId): Seq[UniqueUserId] = + taggedUsers.getOrElse(tagId, Set[UniqueUserId]()).toVector + + override def getAllTags: Seq[UserTag] = tags.values.toVector + + override def tagUser( + tagId: UserTagId, + uniqueUserId: UniqueUserId + ): Option[(UserTagId, UniqueUserId)] = + taggedUsers.get(tagId) match { + case Some(users) => + taggedUsers += (tagId -> (users + uniqueUserId)) + Some((tagId, uniqueUserId)) + case None => None + } + + override def untagUser( + tagId: UserTagId, + uniqueUserId: UniqueUserId + ): Option[(UserTagId, UniqueUserId)] = + taggedUsers.get(tagId) match { + case Some(users) => + taggedUsers += (tagId -> (users - uniqueUserId)) + Some((tagId, uniqueUserId)) + case None => None + } +} diff --git a/src/main/scala/ru/otus/sc/user/implicits/UserNameImplicits.scala b/src/main/scala/ru/otus/sc/user/implicits/UserNameImplicits.scala new file mode 100644 index 0000000..b817d05 --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/implicits/UserNameImplicits.scala @@ -0,0 +1,16 @@ +package ru.otus.sc.user.implicits + +import ru.otus.sc.user.model.User + +object UserNameImplicits { + implicit def userNameToString(user: User): String = + List( + user.name.title, + Some(user.name.firstName), + user.name.middleName, + user.name.patronymicName, + user.name.lastName + ) + .collect { case Some(name) => name } + .mkString(" ") +} diff --git a/src/main/scala/ru/otus/sc/user/model/CreateTag.scala b/src/main/scala/ru/otus/sc/user/model/CreateTag.scala new file mode 100644 index 0000000..61a407e --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/model/CreateTag.scala @@ -0,0 +1,9 @@ +package ru.otus.sc.user.model + +case class CreateUserTagRequest(tag: UserTag) + +sealed trait CreateUserTagResponse +object CreateUserTagResponse { + case class Created(tag: UserTag) extends CreateUserTagResponse + case class Error(error: String) extends CreateUserTagResponse +} diff --git a/src/main/scala/ru/otus/sc/user/model/CreateUser.scala b/src/main/scala/ru/otus/sc/user/model/CreateUser.scala new file mode 100644 index 0000000..e21327b --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/model/CreateUser.scala @@ -0,0 +1,9 @@ +package ru.otus.sc.user.model + +case class CreateUserRequest(user: User) + +sealed trait CreateUserResponse +object CreateUserResponse { + case class Created(user: User) extends CreateUserResponse + case class Error(error: String) extends CreateUserResponse +} diff --git a/src/main/scala/ru/otus/sc/user/model/DeleteTag.scala b/src/main/scala/ru/otus/sc/user/model/DeleteTag.scala new file mode 100644 index 0000000..ff40123 --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/model/DeleteTag.scala @@ -0,0 +1,11 @@ +package ru.otus.sc.user.model + +import ru.otus.sc.user.model.UserTag.UserTagId + +case class DeleteUserTagRequest(tagId: UserTagId) + +sealed trait DeleteUserTagResponse +object DeleteUserTagResponse { + case class Deleted(tag: UserTag) extends DeleteUserTagResponse + case class NotFound(tagId: UserTagId) extends DeleteUserTagResponse +} diff --git a/src/main/scala/ru/otus/sc/user/model/DeleteUser.scala b/src/main/scala/ru/otus/sc/user/model/DeleteUser.scala new file mode 100644 index 0000000..1be5da1 --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/model/DeleteUser.scala @@ -0,0 +1,11 @@ +package ru.otus.sc.user.model + +import ru.otus.sc.user.model.User.UniqueUserId + +case class DeleteUserRequest(uniqueId: UniqueUserId) + +sealed trait DeleteUserResponse +object DeleteUserResponse { + case class Deleted(user: User) extends DeleteUserResponse + case class NotFound(uniqueId: UniqueUserId) extends DeleteUserResponse +} diff --git a/src/main/scala/ru/otus/sc/user/model/FindTags.scala b/src/main/scala/ru/otus/sc/user/model/FindTags.scala new file mode 100644 index 0000000..56d322b --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/model/FindTags.scala @@ -0,0 +1,17 @@ +package ru.otus.sc.user.model + +import ru.otus.sc.user.model.User.UniqueUserId +import ru.otus.sc.user.model.UserTag.UserTagId + +sealed trait FindUserTagsRequest +object FindUserTagsRequest { + case class ByName(tagName: String) extends FindUserTagsRequest + case class UsersByTag(tag: UserTagId) extends FindUserTagsRequest + case class GetAll() extends FindUserTagsRequest +} + +sealed trait FindUserTagsResponse +object FindUserTagsResponse { + case class TagsResult(tags: Seq[UserTag]) extends FindUserTagsResponse + case class UsersResult(users: Seq[UniqueUserId]) extends FindUserTagsResponse +} diff --git a/src/main/scala/ru/otus/sc/user/model/FindUsers.scala b/src/main/scala/ru/otus/sc/user/model/FindUsers.scala new file mode 100644 index 0000000..71efc78 --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/model/FindUsers.scala @@ -0,0 +1,16 @@ +package ru.otus.sc.user.model + +import ru.otus.sc.user.model.UserTag.UserTagId + +sealed trait FindUsersRequest +object FindUsersRequest { + case class ByFirstName(firstName: String) extends FindUsersRequest + case class ByLastName(lastName: String) extends FindUsersRequest + case class ByTag(tag: UserTagId) extends FindUsersRequest + case class GetAll() extends FindUsersRequest +} + +sealed trait FindUsersResponse +object FindUsersResponse { + case class Result(users: Seq[User]) extends FindUsersResponse +} diff --git a/src/main/scala/ru/otus/sc/user/model/GetTag.scala b/src/main/scala/ru/otus/sc/user/model/GetTag.scala new file mode 100644 index 0000000..266da46 --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/model/GetTag.scala @@ -0,0 +1,11 @@ +package ru.otus.sc.user.model + +import ru.otus.sc.user.model.UserTag.UserTagId + +case class GetUserTagRequest(tagId: UserTagId) + +sealed trait GetUserTagResponse +object GetUserTagResponse { + case class Found(tag: UserTag) extends GetUserTagResponse + case class NotFound(tagId: UserTagId) extends GetUserTagResponse +} diff --git a/src/main/scala/ru/otus/sc/user/model/GetUser.scala b/src/main/scala/ru/otus/sc/user/model/GetUser.scala new file mode 100644 index 0000000..045ee00 --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/model/GetUser.scala @@ -0,0 +1,12 @@ +package ru.otus.sc.user.model + +import ru.otus.sc.user.model.User.UniqueUserId + +case class GetUserRequest(uniqueId: UniqueUserId) + +sealed trait GetUserResponse +object GetUserResponse { + case class Found(user: User) extends GetUserResponse + case class NotFound(uniqueId: UniqueUserId) extends GetUserResponse + +} diff --git a/src/main/scala/ru/otus/sc/user/model/UpdateTag.scala b/src/main/scala/ru/otus/sc/user/model/UpdateTag.scala new file mode 100644 index 0000000..716131b --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/model/UpdateTag.scala @@ -0,0 +1,12 @@ +package ru.otus.sc.user.model + +import ru.otus.sc.user.model.UserTag.UserTagId + +case class UpdateUserTagRequest(tag: UserTag) + +sealed trait UpdateUserTagResponse +object UpdateUserTagResponse { + case class Updated(tag: UserTag) extends UpdateUserTagResponse + case class NotFound(tagId: UserTagId) extends UpdateUserTagResponse + case object ErrorNoTagId extends UpdateUserTagResponse +} diff --git a/src/main/scala/ru/otus/sc/user/model/UpdateTagOnUsers.scala b/src/main/scala/ru/otus/sc/user/model/UpdateTagOnUsers.scala new file mode 100644 index 0000000..3bfb28c --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/model/UpdateTagOnUsers.scala @@ -0,0 +1,21 @@ +package ru.otus.sc.user.model + +import ru.otus.sc.user.model.User.UniqueUserId +import ru.otus.sc.user.model.UserTag.UserTagId + +case class UpdateTagUserRequest(tagId: UserTagId, uniqueUserId: UniqueUserId) + +sealed trait UpdateTagUserResponse +object UpdateTagUserResponse { + case class TaggedUser(tagId: UserTagId, uniqueUserId: UniqueUserId) extends UpdateTagUserResponse + case class NotFoundTag(tagId: UserTagId) extends UpdateTagUserResponse +} + +case class UpdateUntagUserRequest(tagId: UserTagId, uniqueUserId: UniqueUserId) + +sealed trait UpdateUntagUserResponse +object UpdateUntagUserResponse { + case class UntaggedUser(tagId: UserTagId, uniqueUserId: UniqueUserId) + extends UpdateUntagUserResponse + case class NotFoundTag(tagId: UserTagId) extends UpdateUntagUserResponse +} diff --git a/src/main/scala/ru/otus/sc/user/model/UpdateUser.scala b/src/main/scala/ru/otus/sc/user/model/UpdateUser.scala new file mode 100644 index 0000000..fca1437 --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/model/UpdateUser.scala @@ -0,0 +1,13 @@ +package ru.otus.sc.user.model + +import ru.otus.sc.user.model.User.UniqueUserId + +case class UpdateUserRequest(user: User) + +sealed trait UpdateUserResponse +object UpdateUserResponse { + case class Updated(user: User) extends UpdateUserResponse + case class NotFound(uniqueId: UniqueUserId) extends UpdateUserResponse + case object ErrorNoUniqueId extends UpdateUserResponse + case object ErrorWrongId extends UpdateUserResponse +} diff --git a/src/main/scala/ru/otus/sc/user/model/User.scala b/src/main/scala/ru/otus/sc/user/model/User.scala new file mode 100644 index 0000000..f2aaef9 --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/model/User.scala @@ -0,0 +1,28 @@ +package ru.otus.sc.user.model + +import java.util.UUID + +import ru.otus.sc.user.model.User.UniqueUserId +import ru.otus.sc.user.model.UserTag.UserTagId + +case class UserName( + firstName: String, + lastName: Option[String], + middleName: Option[String], + patronymicName: Option[String], + title: Option[String] +) + +case class User( + // id related to DB table + id: Option[Long], + // unique id for whole system + uniqueId: Option[UniqueUserId], + name: UserName, + age: Option[Int], + tags: Set[UserTagId] +) + +object User { + type UniqueUserId = UUID +} diff --git a/src/main/scala/ru/otus/sc/user/model/UserTag.scala b/src/main/scala/ru/otus/sc/user/model/UserTag.scala new file mode 100644 index 0000000..70ee550 --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/model/UserTag.scala @@ -0,0 +1,9 @@ +package ru.otus.sc.user.model + +import ru.otus.sc.user.model.UserTag.UserTagId + +case class UserTag(id: Option[UserTagId], tagName: String) + +object UserTag { + type UserTagId = Long +} diff --git a/src/main/scala/ru/otus/sc/user/service/UserService.scala b/src/main/scala/ru/otus/sc/user/service/UserService.scala new file mode 100644 index 0000000..de1f57d --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/service/UserService.scala @@ -0,0 +1,11 @@ +package ru.otus.sc.user.service + +import ru.otus.sc.user.model._ + +trait UserService { + def createUser(request: CreateUserRequest): CreateUserResponse + def getUser(request: GetUserRequest): GetUserResponse + def updateUser(request: UpdateUserRequest): UpdateUserResponse + def deleteUser(request: DeleteUserRequest): DeleteUserResponse + def findUsers(request: FindUsersRequest): FindUsersResponse +} diff --git a/src/main/scala/ru/otus/sc/user/service/UserTagService.scala b/src/main/scala/ru/otus/sc/user/service/UserTagService.scala new file mode 100644 index 0000000..2f85871 --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/service/UserTagService.scala @@ -0,0 +1,13 @@ +package ru.otus.sc.user.service + +import ru.otus.sc.user.model._ + +trait UserTagService { + def createUserTag(request: CreateUserTagRequest): CreateUserTagResponse + def getUserTag(request: GetUserTagRequest): GetUserTagResponse + def updateUserTag(request: UpdateUserTagRequest): UpdateUserTagResponse + def deleteUserTag(request: DeleteUserTagRequest): DeleteUserTagResponse + def findUserTags(request: FindUserTagsRequest): FindUserTagsResponse + def tagUser(request: UpdateTagUserRequest): UpdateTagUserResponse + def untagUser(request: UpdateUntagUserRequest): UpdateUntagUserResponse +} diff --git a/src/main/scala/ru/otus/sc/user/service/impl/UserServiceImpl.scala b/src/main/scala/ru/otus/sc/user/service/impl/UserServiceImpl.scala new file mode 100644 index 0000000..fbcaa27 --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/service/impl/UserServiceImpl.scala @@ -0,0 +1,56 @@ +package ru.otus.sc.user.service.impl + +import ru.otus.sc.user.dao.UserDao +import ru.otus.sc.user.model._ +import ru.otus.sc.user.service.UserService + +class UserServiceImpl(dao: UserDao) extends UserService { + def createUser(request: CreateUserRequest): CreateUserResponse = { + // it can return CreateUserResponse.Error + // but in current UserDao implementation there is not option for this + // so emulate it with palindrome or empty name + val userName = request.user.name.firstName + if (userName == userName.reverse) + CreateUserResponse.Error("palindrome or empty name is not allowed in current DB version") + else + CreateUserResponse.Created(dao.createUser(request.user)) + } + + def getUser(request: GetUserRequest): GetUserResponse = + dao.getUser(request.uniqueId) match { + case Some(user) => GetUserResponse.Found(user) + case None => GetUserResponse.NotFound(request.uniqueId) + } + + def updateUser(request: UpdateUserRequest): UpdateUserResponse = + request.user.uniqueId match { + case Some(uniqueId) => + dao.updateUser(request.user) match { + case Some(user) => UpdateUserResponse.Updated(user) + case None => UpdateUserResponse.NotFound(uniqueId) + } + case None => + if (request.user.id.isEmpty) + UpdateUserResponse.ErrorWrongId + else + UpdateUserResponse.ErrorNoUniqueId + } + + def deleteUser(request: DeleteUserRequest): DeleteUserResponse = + dao + .deleteUser(request.uniqueId) + .map(DeleteUserResponse.Deleted) + .getOrElse(DeleteUserResponse.NotFound(request.uniqueId)) + + def findUsers(request: FindUsersRequest): FindUsersResponse = + request match { + case FindUsersRequest.ByFirstName(firstName) => + FindUsersResponse.Result(dao.findByFirstName(firstName)) + case FindUsersRequest.ByLastName(lastName) => + FindUsersResponse.Result(dao.findByLastName(lastName)) + case FindUsersRequest.ByTag(tagId) => + FindUsersResponse.Result(dao.findByTag(tagId)) + case FindUsersRequest.GetAll() => + FindUsersResponse.Result(dao.getAllUsers) + } +} diff --git a/src/main/scala/ru/otus/sc/user/service/impl/UserTagServiceImpl.scala b/src/main/scala/ru/otus/sc/user/service/impl/UserTagServiceImpl.scala new file mode 100644 index 0000000..9091c19 --- /dev/null +++ b/src/main/scala/ru/otus/sc/user/service/impl/UserTagServiceImpl.scala @@ -0,0 +1,64 @@ +package ru.otus.sc.user.service.impl + +import ru.otus.sc.user.dao.UserTagDao +import ru.otus.sc.user.model._ +import ru.otus.sc.user.service.UserTagService + +class UserTagServiceImpl(dao: UserTagDao) extends UserTagService { + def createUserTag(request: CreateUserTagRequest): CreateUserTagResponse = { + // it can return CreateUserTagResponse.Error + // but in current UserTagDao implementation there is not option for this + // so emulate it with palindrome or empty tag name + val tagName = request.tag.tagName + if (tagName == tagName.reverse) + CreateUserTagResponse.Error( + "palindrome or empty tag name is not allowed in current DB version" + ) + else + CreateUserTagResponse.Created(dao.createTag(request.tag)) + } + + def getUserTag(request: GetUserTagRequest): GetUserTagResponse = + dao.getTag(request.tagId) match { + case Some(user) => GetUserTagResponse.Found(user) + case None => GetUserTagResponse.NotFound(request.tagId) + } + + def updateUserTag(request: UpdateUserTagRequest): UpdateUserTagResponse = + request.tag.id match { + case Some(tagId) => + dao.updateTag(request.tag) match { + case Some(tag) => UpdateUserTagResponse.Updated(tag) + case None => UpdateUserTagResponse.NotFound(tagId) + } + case None => UpdateUserTagResponse.ErrorNoTagId + } + + def deleteUserTag(request: DeleteUserTagRequest): DeleteUserTagResponse = + dao + .deleteTag(request.tagId) + .map(DeleteUserTagResponse.Deleted) + .getOrElse(DeleteUserTagResponse.NotFound(request.tagId)) + + def findUserTags(request: FindUserTagsRequest): FindUserTagsResponse = + request match { + case FindUserTagsRequest.ByName(tagName) => + FindUserTagsResponse.TagsResult(dao.findByName(tagName)) + case FindUserTagsRequest.UsersByTag(tagId) => + FindUserTagsResponse.UsersResult(dao.getUsersByTag(tagId)) + case FindUserTagsRequest.GetAll() => + FindUserTagsResponse.TagsResult(dao.getAllTags) + } + + override def tagUser(request: UpdateTagUserRequest): UpdateTagUserResponse = + dao + .tagUser(request.tagId, request.uniqueUserId) + .map(result => UpdateTagUserResponse.TaggedUser(result._1, result._2)) + .getOrElse(UpdateTagUserResponse.NotFoundTag(request.tagId)) + + def untagUser(request: UpdateUntagUserRequest): UpdateUntagUserResponse = + dao + .untagUser(request.tagId, request.uniqueUserId) + .map(result => UpdateUntagUserResponse.UntaggedUser(result._1, result._2)) + .getOrElse(UpdateUntagUserResponse.NotFoundTag(request.tagId)) +}