Skip to content

Commit

Permalink
Allow adding participants by username to recruitments
Browse files Browse the repository at this point in the history
This includes a `RecruitmentService` API upgrade and a migration to convert `RecruitmentService.AddParticipant`
requests into `RecruitmentService.AddParticipantByEmailAddress` requests.
  • Loading branch information
xelahalo authored and Whathecode committed Mar 16, 2024
1 parent bb9ff20 commit 5d3ee7c
Show file tree
Hide file tree
Showing 30 changed files with 2,217 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dk.cachet.carp.common.application.services.ApiVersion
import dk.cachet.carp.common.application.services.ApplicationService
import dk.cachet.carp.common.application.services.DependentServices
import dk.cachet.carp.common.application.services.IntegrationEvent
import dk.cachet.carp.common.application.users.Username
import dk.cachet.carp.studies.application.users.AssignedParticipantRoles
import dk.cachet.carp.studies.application.users.Participant
import dk.cachet.carp.studies.application.users.ParticipantGroupStatus
Expand All @@ -19,7 +20,7 @@ import kotlinx.serialization.*
@DependentServices( StudyService::class )
interface RecruitmentService : ApplicationService<RecruitmentService, RecruitmentService.Event>
{
companion object { val API_VERSION = ApiVersion( 1, 0 ) }
companion object { val API_VERSION = ApiVersion( 1, 2 ) }

@Serializable
sealed class Event : IntegrationEvent<RecruitmentService>
Expand All @@ -37,6 +38,14 @@ interface RecruitmentService : ApplicationService<RecruitmentService, Recruitmen
*/
suspend fun addParticipant( studyId: UUID, email: EmailAddress ): Participant

/**
* Add a [Participant] to the study with the specified [studyId], identified by the specified [username].
* In case the [username] was already added before, the same [Participant] is returned.
*
* @throws IllegalArgumentException when a study with [studyId] does not exist.
*/
suspend fun addParticipant( studyId: UUID, username: Username ): Participant

/**
* Returns a participant of a study with the specified [studyId], identified by [participantId].
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import dk.cachet.carp.common.application.EmailAddress
import dk.cachet.carp.common.application.UUID
import dk.cachet.carp.common.application.UUIDFactory
import dk.cachet.carp.common.application.services.ApplicationServiceEventBus
import dk.cachet.carp.common.application.users.AccountIdentity
import dk.cachet.carp.common.application.users.EmailAccountIdentity
import dk.cachet.carp.common.application.users.Username
import dk.cachet.carp.common.application.users.UsernameAccountIdentity
import dk.cachet.carp.deployments.application.DeploymentService
import dk.cachet.carp.deployments.application.StudyDeploymentStatus
import dk.cachet.carp.studies.application.users.AssignedParticipantRoles
Expand Down Expand Up @@ -61,11 +65,23 @@ class RecruitmentServiceHost(
*
* @throws IllegalArgumentException when a study with [studyId] does not exist.
*/
override suspend fun addParticipant( studyId: UUID, email: EmailAddress ): Participant
override suspend fun addParticipant( studyId: UUID, email: EmailAddress ): Participant =
addParticipant( studyId, EmailAccountIdentity( email ) )

/**
* Add a [Participant] to the study with the specified [studyId], identified by the specified [username].
* In case the [username] was already added before, the same [Participant] is returned.
*
* @throws IllegalArgumentException when a study with [studyId] does not exist.
*/
override suspend fun addParticipant( studyId: UUID, username: Username ): Participant =
addParticipant( studyId, UsernameAccountIdentity( username ) )

private suspend fun addParticipant( studyId: UUID, accountIdentity: AccountIdentity ): Participant
{
val recruitment = getRecruitmentOrThrow( studyId )

val participant = recruitment.addParticipant( email, uuidFactory.randomUUID() )
val participant = recruitment.addParticipant( accountIdentity, uuidFactory.randomUUID() )
participantRepository.updateRecruitment( recruitment )

return participant
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package dk.cachet.carp.studies.domain.users

import dk.cachet.carp.common.application.EmailAddress
import dk.cachet.carp.common.application.UUID
import dk.cachet.carp.common.application.users.AccountIdentity
import dk.cachet.carp.common.application.users.EmailAccountIdentity
import dk.cachet.carp.common.application.users.UsernameAccountIdentity
import dk.cachet.carp.common.domain.AggregateRoot
import dk.cachet.carp.common.domain.DomainEvent
import dk.cachet.carp.deployments.application.StudyDeploymentStatus
Expand Down Expand Up @@ -59,13 +61,12 @@ class Recruitment( val studyId: UUID, id: UUID = UUID.randomUUID(), createdOn: I
get() = _participants.toSet()

/**
* Add a [Participant] identified by the specified [email] address.
* In case the [email] was already added before, the same [Participant] is returned.
* Add a [Participant] by the specified [identity].
* In case a participant with [identity] was already added before, the same [Participant] is returned.
*/
fun addParticipant( email: EmailAddress, id: UUID = UUID.randomUUID() ): Participant
fun addParticipant( identity: AccountIdentity, id: UUID = UUID.randomUUID() ): Participant
{
// Verify whether participant was already added.
val identity = EmailAccountIdentity( email )
var participant = _participants.firstOrNull { it.accountIdentity == identity }

// Add new participant in case it was not added before.
Expand All @@ -79,6 +80,21 @@ class Recruitment( val studyId: UUID, id: UUID = UUID.randomUUID(), createdOn: I
return participant
}

/**
* Add a [Participant] by the specified [email].
* In case a participant with the same [email] was already added before, the same [Participant] is returned.
*/
fun addParticipant( email: EmailAddress, id: UUID = UUID.randomUUID() ): Participant =
addParticipant( EmailAccountIdentity( email ), id )

/**
* Add a [Participant] by the specified [username].
* In case a participant with the same [username] was already added before, the same [Participant] is returned.
*/
fun addParticipant( username: String, id: UUID = UUID.randomUUID() ): Participant =
addParticipant( UsernameAccountIdentity( username ), id )


private var studyProtocol: StudyProtocolSnapshot? = null
private var invitation: StudyInvitation? = null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package dk.cachet.carp.studies.infrastructure

import dk.cachet.carp.common.application.EmailAddress
import dk.cachet.carp.common.application.UUID
import dk.cachet.carp.common.application.users.Username
import dk.cachet.carp.common.infrastructure.services.ApplicationServiceDecorator
import dk.cachet.carp.common.infrastructure.services.ApplicationServiceInvoker
import dk.cachet.carp.common.infrastructure.services.Command
import dk.cachet.carp.studies.application.RecruitmentService
import dk.cachet.carp.studies.application.users.AssignedParticipantRoles
import dk.cachet.carp.studies.application.users.Participant


class RecruitmentServiceDecorator(
Expand All @@ -20,7 +22,10 @@ class RecruitmentServiceDecorator(
RecruitmentService
{
override suspend fun addParticipant( studyId: UUID, email: EmailAddress ) =
invoke( RecruitmentServiceRequest.AddParticipant( studyId, email ) )
invoke( RecruitmentServiceRequest.AddParticipantByEmailAddress( studyId, email ) )

override suspend fun addParticipant( studyId: UUID, username: Username ): Participant =
invoke( RecruitmentServiceRequest.AddParticipantByUsername( studyId, username ) )

override suspend fun getParticipant( studyId: UUID, participantId: UUID ) =
invoke( RecruitmentServiceRequest.GetParticipant( studyId, participantId ) )
Expand All @@ -46,7 +51,8 @@ object RecruitmentServiceInvoker : ApplicationServiceInvoker<RecruitmentService,
override suspend fun RecruitmentServiceRequest<*>.invoke( service: RecruitmentService ): Any =
when ( this )
{
is RecruitmentServiceRequest.AddParticipant -> service.addParticipant( studyId, email )
is RecruitmentServiceRequest.AddParticipantByEmailAddress -> service.addParticipant( studyId, email )
is RecruitmentServiceRequest.AddParticipantByUsername -> service.addParticipant( studyId, username )
is RecruitmentServiceRequest.GetParticipant -> service.getParticipant( studyId, participantId )
is RecruitmentServiceRequest.GetParticipants -> service.getParticipants( studyId )
is RecruitmentServiceRequest.InviteNewParticipantGroup ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dk.cachet.carp.studies.infrastructure
import dk.cachet.carp.common.application.EmailAddress
import dk.cachet.carp.common.application.UUID
import dk.cachet.carp.common.application.services.ApiVersion
import dk.cachet.carp.common.application.users.Username
import dk.cachet.carp.common.infrastructure.serialization.ignoreTypeParameters
import dk.cachet.carp.common.infrastructure.services.ApplicationServiceRequest
import dk.cachet.carp.studies.application.RecruitmentService
Expand All @@ -11,6 +12,8 @@ import dk.cachet.carp.studies.application.users.Participant
import dk.cachet.carp.studies.application.users.ParticipantGroupStatus
import kotlinx.serialization.*
import kotlin.js.JsExport
import kotlin.reflect.KCallable
import kotlin.reflect.KSuspendFunction3


/**
Expand All @@ -28,9 +31,25 @@ sealed class RecruitmentServiceRequest<out TReturn> : ApplicationServiceRequest<


@Serializable
data class AddParticipant( val studyId: UUID, val email: EmailAddress ) : RecruitmentServiceRequest<Participant>()
data class AddParticipantByEmailAddress( val studyId: UUID, val email: EmailAddress ) :
RecruitmentServiceRequest<Participant>()
{
override fun getResponseSerializer() = serializer<Participant>()
override fun matchesServiceRequest( request: KCallable<*> ): Boolean =
request == run<KSuspendFunction3<RecruitmentService, UUID, EmailAddress, Participant>> {
RecruitmentService::addParticipant
}
}

@Serializable
data class AddParticipantByUsername( val studyId: UUID, val username: Username ) :
RecruitmentServiceRequest<Participant>()
{
override fun getResponseSerializer() = serializer<Participant>()
override fun matchesServiceRequest( request: KCallable<*> ): Boolean =
request == run<KSuspendFunction3<RecruitmentService, UUID, Username, Participant>> {
RecruitmentService::addParticipant
}
}

@Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,40 @@
package dk.cachet.carp.studies.infrastructure.versioning

import dk.cachet.carp.common.application.services.ApiVersion
import dk.cachet.carp.common.infrastructure.versioning.ApiMigration
import dk.cachet.carp.common.infrastructure.versioning.ApiResponse
import dk.cachet.carp.common.infrastructure.versioning.ApplicationServiceApiMigrator
import dk.cachet.carp.studies.application.RecruitmentService
import dk.cachet.carp.studies.infrastructure.RecruitmentServiceInvoker
import dk.cachet.carp.studies.infrastructure.RecruitmentServiceRequest
import kotlinx.serialization.json.JsonObject


const val recruitmentRequest = "dk.cachet.carp.studies.infrastructure.RecruitmentServiceRequest"

private val major1Minor0To2Migration =
object : ApiMigration( 0, 2 )
{
override fun migrateRequest( request: JsonObject ): JsonObject = request.migrate {
ifType( "$recruitmentRequest.AddParticipant" )
{
changeType( "$recruitmentRequest.AddParticipantByEmailAddress" )
}
}

override fun migrateResponse(
request: JsonObject,
response: ApiResponse,
targetVersion: ApiVersion
): ApiResponse = response

override fun migrateEvent( event: JsonObject ): JsonObject = event
}

val RecruitmentServiceApiMigrator = ApplicationServiceApiMigrator(
RecruitmentService.API_VERSION,
RecruitmentServiceInvoker,
RecruitmentServiceRequest.Serializer,
RecruitmentService.Event.serializer()
RecruitmentService.Event.serializer(),
listOf( major1Minor0To2Migration )
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import dk.cachet.carp.common.application.users.AssignedTo
import dk.cachet.carp.common.application.users.ExpectedParticipantData
import dk.cachet.carp.common.application.users.ParticipantAttribute
import dk.cachet.carp.common.application.users.ParticipantRole
import dk.cachet.carp.common.application.users.Username
import dk.cachet.carp.protocols.application.StudyProtocolSnapshot
import dk.cachet.carp.protocols.domain.StudyProtocol
import dk.cachet.carp.studies.application.users.AssignedParticipantRoles
Expand Down Expand Up @@ -41,20 +42,21 @@ interface RecruitmentServiceTest


@Test
fun adding_and_retrieving_participant_succeeds() = runTest {
fun adding_and_retrieving_participants_succeeds() = runTest {
val (recruitmentService, studyService) = createSUT()
val study = studyService.createStudy( UUID.randomUUID(), "Test" )
val studyId = study.studyId

val participant = recruitmentService.addParticipant( studyId, EmailAddress( "[email protected]" ) )
val emailParticipant = recruitmentService.addParticipant( studyId, EmailAddress( "[email protected]" ) )
val usernameParticipant = recruitmentService.addParticipant( studyId, Username( "test" ) )

// Get single participant.
val studyParticipant = recruitmentService.getParticipant( studyId, participant.id )
assertEquals( participant, studyParticipant )
// Get participants by ID.
assertEquals( emailParticipant, recruitmentService.getParticipant( studyId, emailParticipant.id ) )
assertEquals( usernameParticipant, recruitmentService.getParticipant( studyId, usernameParticipant.id ) )

// Get all participants.
val studyParticipants = recruitmentService.getParticipants( studyId )
assertEquals( participant, studyParticipants.single() )
assertEquals( setOf( emailParticipant, usernameParticipant ), studyParticipants.toSet() )
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dk.cachet.carp.studies.infrastructure

import dk.cachet.carp.common.application.EmailAddress
import dk.cachet.carp.common.application.UUID
import dk.cachet.carp.common.application.users.Username
import dk.cachet.carp.common.test.infrastructure.ApplicationServiceDecoratorTest
import dk.cachet.carp.common.test.infrastructure.ApplicationServiceRequestsTest
import dk.cachet.carp.studies.application.RecruitmentService
Expand All @@ -19,7 +20,8 @@ class RecruitmentServiceRequestsTest : ApplicationServiceRequestsTest<Recruitmen
private val studyId = UUID.randomUUID()

val REQUESTS: List<RecruitmentServiceRequest<*>> = listOf(
RecruitmentServiceRequest.AddParticipant( studyId, EmailAddress( "[email protected]" ) ),
RecruitmentServiceRequest.AddParticipantByEmailAddress( studyId, EmailAddress( "[email protected]" ) ),
RecruitmentServiceRequest.AddParticipantByUsername( studyId, Username( "test" ) ),
RecruitmentServiceRequest.GetParticipant( studyId, UUID.randomUUID() ),
RecruitmentServiceRequest.GetParticipants( studyId ),
RecruitmentServiceRequest.InviteNewParticipantGroup( studyId, setOf() ),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[
{
"outcome": "Failed",
"request": {
"__type": "dk.cachet.carp.studies.infrastructure.RecruitmentServiceRequest.AddParticipantByEmailAddress",
"apiVersion": "1.1",
"studyId": "1bd9db94-50a4-409e-a189-ef3bd2ec4bee",
"email": "[email protected]"
},
"precedingEvents": [
],
"publishedEvents": [
],
"exceptionType": "IllegalArgumentException"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
[
{
"outcome": "Succeeded",
"request": {
"__type": "dk.cachet.carp.studies.infrastructure.RecruitmentServiceRequest.AddParticipantByEmailAddress",
"apiVersion": "1.1",
"studyId": "00000000-0000-0000-0000-000000000001",
"email": "[email protected]"
},
"precedingEvents": [
{
"__type": "dk.cachet.carp.studies.application.StudyService.Event.StudyCreated",
"aggregateId": "00000000-0000-0000-0000-000000000001",
"apiVersion": "1.1",
"study": {
"studyId": "00000000-0000-0000-0000-000000000001",
"ownerId": "8b538dc7-edb3-4365-9fdc-819daba1485d",
"name": "Test",
"createdOn": "1970-01-01T00:00:00Z",
"description": null,
"invitation": {
"name": "Test"
},
"protocolSnapshot": null
}
}
],
"publishedEvents": [
],
"response": {
"accountIdentity": {
"__type": "dk.cachet.carp.common.application.users.EmailAccountIdentity",
"emailAddress": "[email protected]"
},
"id": "00000000-0000-0000-0000-000000000002"
}
},
{
"outcome": "Succeeded",
"request": {
"__type": "dk.cachet.carp.studies.infrastructure.RecruitmentServiceRequest.AddParticipantByEmailAddress",
"apiVersion": "1.1",
"studyId": "00000000-0000-0000-0000-000000000001",
"email": "[email protected]"
},
"precedingEvents": [
],
"publishedEvents": [
],
"response": {
"accountIdentity": {
"__type": "dk.cachet.carp.common.application.users.EmailAccountIdentity",
"emailAddress": "[email protected]"
},
"id": "00000000-0000-0000-0000-000000000002"
}
}
]
Loading

0 comments on commit 5d3ee7c

Please sign in to comment.