Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
refactor(user): add user project (DEV-586) (#2063)
* add user project

* add simple UserHandler methods

* add unit test for user repo

* add new structure for tests

* refactoring some code

* clean up code

* run UserRepoSpec for all implementations

* add more methods and tests to user handler

* restructure code

* fix test

* update structure of user slice

* test: fix race condition by providing different environments to each test

* wip

* fix failing tests

* update dependencies for shared project

* add method to check if username or email is taken

* improve UserUserHandlerSpec

* organize imports

Co-authored-by: Balduin Landolt <33053745+BalduinLandolt@users.noreply.github.com>
Co-authored-by: Marcin Procyk <marcin.procyk@dasch.swiss>
  • Loading branch information
3 people committed Jun 13, 2022
1 parent 96362f4 commit 0c5ec03
Show file tree
Hide file tree
Showing 19 changed files with 924 additions and 11 deletions.
79 changes: 79 additions & 0 deletions build.sbt
Expand Up @@ -240,6 +240,18 @@ lazy val apiMain = project
)
.dependsOn(schemaCore, schemaRepo, schemaApi)

// Value Objects project

lazy val valueObjects = project
.in(file("dsp-value-objects"))
.settings(
name := "valueObjects",
libraryDependencies ++= Dependencies.valueObjectsLibraryDependencies,
testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework"))
)

// Schema projects

lazy val schemaApi = project
.in(file("dsp-schema/api"))
.settings(
Expand Down Expand Up @@ -284,6 +296,73 @@ lazy val schemaRepoSearchService = project
)
.dependsOn(schemaRepo)

// User projects

lazy val userInterface = project
.in(file("dsp-user/interface"))
.settings(
scalacOptions ++= Seq(
"-feature",
"-unchecked",
"-deprecation",
"-Yresolve-term-conflict:package",
"-Ymacro-annotations"
),
name := "userInterface",
libraryDependencies ++= Dependencies.userInterfaceLibraryDependencies,
testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework"))
)
.dependsOn(shared, userHandler)

lazy val userHandler = project
.in(file("dsp-user/handler"))
.settings(
scalacOptions ++= Seq(
"-feature",
"-unchecked",
"-deprecation",
"-Yresolve-term-conflict:package",
"-Ymacro-annotations"
),
name := "userHandler",
libraryDependencies ++= Dependencies.userHandlerLibraryDependencies,
testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework"))
)
.dependsOn(shared, userCore, userRepo % "test->test") //userHandler tests need mock implementation of UserRepo

lazy val userCore = project
.in(file("dsp-user/core"))
.settings(
scalacOptions ++= Seq(
"-feature",
"-unchecked",
"-deprecation",
"-Yresolve-term-conflict:package",
"-Ymacro-annotations"
),
name := "userCore",
libraryDependencies ++= Dependencies.userCoreLibraryDependencies,
testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework"))
)
.dependsOn(shared)

lazy val userRepo = project
.in(file("dsp-user/repo"))
.settings(
scalacOptions ++= Seq(
"-feature",
"-unchecked",
"-deprecation",
"-Yresolve-term-conflict:package",
"-Ymacro-annotations"
),
name := "userRepo",
libraryDependencies ++= Dependencies.userRepoLibraryDependencies,
testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework"))
)
.dependsOn(shared, userCore)
//.dependsOn(userHandler % "compile->compile;test->test", userDomain)

lazy val shared = project
.in(file("dsp-shared"))
.settings(
Expand Down
70 changes: 70 additions & 0 deletions dsp-user/core/src/main/scala/dsp/user/api/UserRepo.scala
@@ -0,0 +1,70 @@
/*
* Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
* SPDX-License-Identifier: Apache-2.0
*/

package dsp.user.api

import dsp.errors._
import dsp.user.domain._
import zio._
import zio.macros.accessible

import java.util.UUID

/**
* The trait (interface) for the user repository. The user repository is responsible for storing and retrieving users.
* Needs to be used by the user repository implementations.
*/
@accessible // with this annotation we don't have to write the companion object ourselves
trait UserRepo {

/**
* Writes a user to the repository (used for both create and update).
* If this fails (e.g. the triplestore is not available), it's a non-recovable error. That's why we need UIO.
* When used, we should do it like: ...store(...).orDie
*
* @param user the user to write
* @return Unit
*/
def storeUser(user: User): UIO[UserId]

/**
* Gets all users from the repository.
*
* @return a list of [[User]]
*/
def getUsers(): UIO[List[User]]

/**
* Retrieves the user from the repository by ID.
*
* @param id the user's ID
* @return an optional [[User]]
*/
def getUserById(id: UserId): IO[Option[Nothing], User]

/**
* Retrieves the user from the repository by username or email.
*
* @param usernameOrEmail username or email of the user.
* @return an optional [[User]].
*/
def getUserByUsernameOrEmail(usernameOrEmail: String): IO[Option[Nothing], User]

/**
* Checks if a username or email exists in the repo.
*
* @param usernameOrEmail username or email of the user.
* @return Unit in case of success
*/
def checkUsernameOrEmailExists(usernameOrEmail: String): IO[Option[Nothing], Unit]

/**
* Deletes a [[User]] from the repository by its [[UserId]].
*
* @param id the user ID
* @return Unit or None if not found
*/
def deleteUser(id: UserId): IO[Option[Nothing], UserId]
}
142 changes: 142 additions & 0 deletions dsp-user/core/src/main/scala/dsp/user/domain/UserDomain.scala
@@ -0,0 +1,142 @@
/*
* Copyright © 2021 - 2022 Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
* SPDX-License-Identifier: Apache-2.0
*/

package dsp.user.domain

import dsp.valueobjects.User._
import zio.prelude.Validation

import java.util.UUID

// move this to shared value objects project once we have it
sealed trait Iri
object Iri {

/**
* UserIri value object.
*/
sealed abstract case class UserIri private (value: String) extends Iri
object UserIri { self =>

def make(value: String): UserIri = new UserIri(value) {}
}
// ...

}

/**
* Stores the user ID, i.e. UUID and IRI of the user
*
* @param uuid the UUID of the user
* @param iri the IRI of the user
*/
abstract case class UserId private (
uuid: UUID,
iri: Iri.UserIri
)

/**
* Companion object for UserId. Contains factory methods for creating UserId instances.
*/
object UserId {

/**
* Generates a UserId instance from a given string (either UUID or IRI).
*
* @param value the string to parse (either UUID or IRI)
* @return a new UserId instance
*/
// TODO not sure if we need this
// def fromString(value: String): UserId = {
// val uuidPattern = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$".r
// val iriPattern = "^http*".r

// value match {
// case uuidPattern(value) => new UserId(UUID.fromString(value), Iri.UserIri.make(value)) {}
// case iriPattern(value) =>
// new UserId(UUID.fromString(value.substring(value.lastIndexOf("/") + 1)), Iri.UserIri.make(value)) {}
// //case _ => ???
// }
// }

/**
* Generates a UserId instance with a new (random) UUID and an IRI which is created from a prefix and the UUID.
*
* @return a new UserId instance
*/
def fromIri(iri: Iri.UserIri): UserId = {
val uuid: UUID = UUID.fromString(iri.value.split("/").last)
new UserId(uuid, iri) {}
}

/**
* Generates a UserId instance with a new (random) UUID and an IRI which is created from a prefix and the UUID.
*
* @return a new UserId instance
*/
def fromUuid(uuid: UUID): UserId = {
val iri: Iri.UserIri = Iri.UserIri.make("http://rdfh.ch/users/" + uuid.toString)
new UserId(uuid, iri) {}
}

/**
* Generates a UserId instance with a new (random) UUID and an IRI which is created from a prefix and the UUID.
*
* @return a new UserId instance
*/
// TODO should this return a Validation[Throwable, UserId]
def make(): UserId = {
val uuid: UUID = UUID.randomUUID()
val iri: Iri.UserIri = Iri.UserIri.make("http://rdfh.ch/users/" + uuid.toString)
new UserId(uuid, iri) {}
}
}

/**
* Represents the user domain object.
*
* @param id the ID of the user
* @param givenName the given name of the user
* @param familyName the family name of the user
* @param username the username of the user
* @param email the email of the user
* @param password the password of the user
* @param language the user's preferred language
* @param role the user's role
*/
sealed abstract case class User private (
id: UserId,
givenName: GivenName,
familyName: FamilyName,
username: Username,
email: Email,
password: Option[Password],
language: LanguageCode
//role: Role
) extends Ordered[User] { self =>

/**
* Allows to sort collections of [[User]]s. Sorting is done by the IRI.
*/
def compare(that: User): Int = self.id.iri.toString().compareTo(that.id.iri.toString())

def updateUsername(value: Username): User =
new User(self.id, self.givenName, self.familyName, value, self.email, self.password, self.language) {}
}
object User {
def make(
givenName: GivenName,
familyName: FamilyName,
username: Username,
email: Email,
password: Password,
language: LanguageCode
//role: Role
): User = {
val id = UserId.make()
new User(id, givenName, familyName, username, email, Some(password), language) {}
}

}
6 changes: 6 additions & 0 deletions dsp-user/core/src/test/scala/dsp/user/api/UserApiSpec.scala
@@ -0,0 +1,6 @@
/*
* Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
* SPDX-License-Identifier: Apache-2.0
*/

package dsp.user.api
@@ -0,0 +1,6 @@
/*
* Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors.
* SPDX-License-Identifier: Apache-2.0
*/

package dsp.user.domain

0 comments on commit 0c5ec03

Please sign in to comment.