Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Activity flow api changes. #2476

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bf1e783
Base classes for the activity flow api
aditya-07 Mar 14, 2024
63c2af7
Merge branch 'master' into ak/activityflow
aditya-07 Mar 15, 2024
38991cb
Updated the api
aditya-07 Mar 26, 2024
ed5c823
Merge branch 'master' into ak/activityflow
aditya-07 Mar 26, 2024
6d9194f
spotless
aditya-07 Mar 26, 2024
de9bbdc
Added fluent api
aditya-07 Mar 27, 2024
a0c1117
Cleanded test
aditya-07 Mar 27, 2024
a12834e
Merge branch 'master' into ak/activityflow
aditya-07 Apr 2, 2024
43eba78
Merge branch 'master' into ak/activityflow
aditya-07 Jun 13, 2024
e12a324
Added more activities
aditya-07 Jun 18, 2024
d322972
Merge branch 'master' into ak/activityflow
aditya-07 Jun 18, 2024
7d0d467
Merge branch 'master' into ak/activityflow
aditya-07 Jun 26, 2024
7e09af2
Updated to a new flow style
aditya-07 Jul 4, 2024
e1eaafc
Merge branch 'master' into ak/activityflow
aditya-07 Jul 4, 2024
e32aaf3
Updated the flow and added CPG structures
aditya-07 Jul 8, 2024
7dd6101
Merge branch 'master' into ak/activityflow
aditya-07 Jul 8, 2024
74c7e1c
Added request and event classes. Updated the flow for event type when…
aditya-07 Jul 15, 2024
9bdf062
Merge branch 'master' into ak/activityflow
aditya-07 Jul 15, 2024
9f056d7
Merge branch 'master' into ak/activityflow
aditya-07 Jul 16, 2024
23f4fd0
Cleanup of activity flow files
aditya-07 Jul 17, 2024
af3c1d6
Merge branch 'master' into ak/activityflow
aditya-07 Jul 19, 2024
0d20d3e
Merge branch 'master' into ak/activityflow
aditya-07 Jul 24, 2024
6a62a6b
Added docs, updated tests
aditya-07 Jul 25, 2024
8567f18
Merge branch 'master' into ak/activityflow
aditya-07 Jul 25, 2024
85c797c
Merge branch 'master' into ak/activityflow
aditya-07 Jul 29, 2024
9e2ad9c
Updated docs
aditya-07 Aug 1, 2024
f3733de
Refactored code , defined new api and separated the phase and resourc…
aditya-07 Sep 12, 2024
9e0626f
Merge branch 'master' into ak/activityflow
aditya-07 Sep 12, 2024
e8013ce
spotless
aditya-07 Sep 12, 2024
e5b7341
Updated kdocs, checks and error messages
aditya-07 Sep 12, 2024
6c9c735
Review comments + kdoc
aditya-07 Sep 12, 2024
031525f
Added tests
aditya-07 Sep 12, 2024
905bc37
Updated some code logic and kdocs
aditya-07 Sep 16, 2024
e582b1b
Refactored code
aditya-07 Sep 16, 2024
0e8bcc7
Review comments: Renamed api, removed classes not related to current…
aditya-07 Sep 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions workflow/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ dependencies {
testImplementation(libs.junit)
testImplementation(libs.truth)
testImplementation(project(":workflow-testing"))
testImplementation(libs.kotlin.test.junit)
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
testImplementation(project(":knowledge"))

configurations.all {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,366 @@
/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.fhir.workflow.activity

import com.google.android.fhir.getResourceClass
import com.google.android.fhir.workflow.activity.phase.Phase
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.ORDER
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.PERFORM
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.PLAN
import com.google.android.fhir.workflow.activity.phase.Phase.PhaseName.PROPOSAL
import com.google.android.fhir.workflow.activity.phase.event.PerformPhase
import com.google.android.fhir.workflow.activity.phase.request.OrderPhase
import com.google.android.fhir.workflow.activity.phase.request.PlanPhase
import com.google.android.fhir.workflow.activity.phase.request.ProposalPhase
import com.google.android.fhir.workflow.activity.resource.event.CPGCommunicationEvent
import com.google.android.fhir.workflow.activity.resource.event.CPGEventResource
import com.google.android.fhir.workflow.activity.resource.event.CPGOrderMedicationEvent
import com.google.android.fhir.workflow.activity.resource.request.CPGCommunicationRequest
import com.google.android.fhir.workflow.activity.resource.request.CPGMedicationRequest
import com.google.android.fhir.workflow.activity.resource.request.CPGRequestResource
import com.google.android.fhir.workflow.activity.resource.request.Intent
import org.hl7.fhir.r4.model.IdType
import org.hl7.fhir.r4.model.Reference
import org.hl7.fhir.r4.model.Resource
import org.opencds.cqf.fhir.api.Repository

internal val Reference.idType
get() = IdType(reference)

internal val Reference.`class`
get() = getResourceClass<Resource>(reference.split("/")[0])
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved

/**
* This abstracts the flow of various
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
* [CPG activities](https://build.fhir.org/ig/HL7/cqf-recommendations/activityflow.html#activity-lifecycle---request-phases-proposal-plan-order)
* throughout their various stages.
*
* The application developer may create the flow of a new proposal and move it through the various
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
* stages or they may resume the workflow of a request already in a later stage (plan,order).
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
*
* The application developers should use the appropriate static factory [ActivityFlow.of] apis
* provided by the library to create or resume an activity flow.
*
* An activity flow starts with the user creating the flow with an appropriate [CPGRequestResource].
* The user may do valid state transitions on the current phase by calling appropriate apis on
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
* current phase. The user may call [getCurrentPhase] as appropriately cast it to [PlanPhase],
* [OrderPhase] or [PerformPhase] by either checking type or value of [Phase.getPhaseName].
*
* The user may then move the activity to the next phase by creating a draft resource by calling
* appropriate draft api([draftPlan], [draftOrder] or [draftPerform]). The user may review and
* update the draft resource and then call appropriate start api([startPlan], [startOrder] or
* [startPerform]) to create a new activity phase.
*
* Since the perform creates a [CPGEventResource] and the same flow could create different event
* resources, application developer needs to provide the appropriate event type as a parameter to
* the [draftPerform].
*
* Example of a `Order a Medication to dispense` where user completes only one [Phase] at a time and
* has to reload / resume the [ActivityFlow] to move to the next [Phase].
*
* ```
* val cpgMedicationRequest =
* CPGMedicationRequest(
* MedicationRequest().apply {
* id = "med-req-01"
* subject = Reference("Patient/pat-01")
* intent = MedicationRequest.MedicationRequestIntent.PROPOSAL
* meta.addProfile("http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-medicationrequest")
* status = MedicationRequest.MedicationRequestStatus.ACTIVE
* addNote(Annotation(MarkdownType("Proposal looks OK.")))
* },
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
* )
*
* val repository = FhirEngineRepository(FhirContext.forR4Cached(), fhirEngine)
* repository.create(cpgMedicationRequest.resource)
*
* val flow = ActivityFlow.of(repository, cpgMedicationRequest)
*
* var cachedRequestId = ""
*
* flow
* .draftPlan()
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
* .onSuccess { draftPlan ->
* draftPlan.update { addNote(Annotation(MarkdownType("Draft plan looks OK."))) }
*
* flow.startPlan(draftPlan).onSuccess { planPhase ->
* val updatedPlan =
* planPhase.getRequest().copy().apply {
* setStatus(Status.ACTIVE)
* update { addNote(Annotation(MarkdownType("Plan looks OK."))) }
* }
*
* planPhase
* .update(updatedPlan)
* .onSuccess { cachedRequestId = updatedPlan.logicalId }
* .onFailure { fail("Should have succeeded", it) }
* }
* }
* .onFailure { fail("Unexpected", it) }
*
* val planToResume =
* repository
* .read(MedicationRequest::class.java, IdType("MedicationRequest", cachedRequestId))
* .let { CPGMedicationRequest(it) }
* val resumedPlanFlow = ActivityFlow.of(repository, planToResume)
*
* check(resumedPlanFlow.getCurrentPhase() is PlanPhase<*>) {
* "Flow is in ${resumedPlanFlow.getCurrentPhase().getPhaseName()} "
* }
* resumedPlanFlow
* .draftOrder()
* .onSuccess { draftOrder ->
* draftOrder.update { addNote(Annotation(MarkdownType("Draft order looks OK."))) }
*
* resumedPlanFlow.startOrder(draftOrder).onSuccess { orderPhase ->
* val updatedOrder =
* orderPhase.getRequest().copy().apply {
* setStatus(Status.ACTIVE)
* update { addNote(Annotation(MarkdownType("Order looks OK."))) }
* }
*
* orderPhase
* .update(updatedOrder)
* .onSuccess { cachedRequestId = updatedOrder.logicalId }
* .onFailure { fail("Should have succeeded", it) }
* }
* }
* .onFailure { fail("Unexpected", it) }
*
* val orderToResume =
* repository
* .read(MedicationRequest::class.java, IdType("MedicationRequest", cachedRequestId))
* .let { CPGMedicationRequest(it) }
*
* val resumedOrderFlow = ActivityFlow.of(repository, orderToResume)
*
* check(resumedOrderFlow.getCurrentPhase() is OrderPhase<*>) {
* "Flow is in ${resumedOrderFlow.getCurrentPhase().getPhaseName()} "
* }
*
* resumedOrderFlow
* .draftPerform(CPGMedicationDispenseEvent::class.java)
* .onSuccess { draftEvent ->
* draftEvent.update { addNote(Annotation(MarkdownType("Draft event looks OK."))) }
*
* resumedOrderFlow.startPerform(draftEvent).onSuccess { performPhase ->
* val updatedEvent =
* performPhase.getEvent().copy().apply {
* setStatus(EventStatus.INPROGRESS)
* update { addNote(Annotation(MarkdownType("Event looks OK."))) }
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
* }
*
* performPhase
* .update(updatedEvent)
* .onSuccess { cachedRequestId = updatedEvent.logicalId }
* .onFailure { fail("Should have succeeded", it) }
* }
* }
* .onFailure { fail("Unexpected", it) }
*
* val eventToResume =
* repository
* .read(MedicationDispense::class.java, IdType("MedicationDispense", cachedRequestId))
* .let { CPGMedicationDispenseEvent(it) }
*
* val resumedPerformFlow = ActivityFlow.of(repository, eventToResume)
*
* check(resumedPerformFlow.getCurrentPhase() is Phase.EventPhase<*>) {
* "Flow is in ${resumedPerformFlow.getCurrentPhase().getPhaseName()} "
* }
*
* (resumedPerformFlow.getCurrentPhase() as Phase.EventPhase<*>).complete()
* ```
*/
@Suppress(
"UnstableApiUsage", /* Repository is marked @Beta */
)
class ActivityFlow<R : CPGRequestResource<*>, E : CPGEventResource<*>>
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you comment on thread-safety of this class ?

private constructor(
private val repository: Repository,
requestResource: R? = null,
eventResource: E? = null,
) {

private var currentPhase: Phase

init {
currentPhase =
if (eventResource != null) {
PerformPhase(repository, eventResource)
} else if (requestResource != null) {
when (requestResource.getIntent()) {
Intent.PROPOSAL -> ProposalPhase(repository, requestResource)
Intent.PLAN -> PlanPhase(repository, requestResource)
Intent.ORDER -> OrderPhase(repository, requestResource)
else ->
throw IllegalArgumentException(
"Couldn't create the flow for ${requestResource.getIntent().code} intent. Supported intents are 'proposal', 'plan' and 'order'.",
)
}
} else {
throw IllegalArgumentException(
"Either Request or Event is required to create a flow. Both can't be null.",
)
}
}

/**
* Returns the current phase of the flow. The users may check the type of flow by calling
* [Phase.getPhaseName] on the [getCurrentPhase] and then cast it to appropriate classes.
*
* The table below shows the mapping between the [PhaseName] and [Phase] implementations.
*
* | [PhaseName] | [Class] |
* |-------------|-----------------|
* | [PROPOSAL] | [ProposalPhase] |
* | [PLAN] | [PlanPhase] |
* | [ORDER] | [OrderPhase] |
* | [PERFORM] | [PerformPhase] |
*/
fun getCurrentPhase(): Phase {
return currentPhase
}

/**
* Creates a draft plan resource based on the state of the [currentPhase].
*
* @return [R] if the action is successful, error otherwise.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct the return kdoc.

Suggested change
* @return [R] if the action is successful, error otherwise.
* @return [Result]<R> containing the draft plan resource if the action is successful, [Result.failure] otherwise.

*/
fun draftPlan(): Result<R> {
aditya-07 marked this conversation as resolved.
Show resolved Hide resolved
return PlanPhase.draft(currentPhase)
}

/**
* Starts a plan phase based on the state of the [currentPhase] and [draftPlan].
*
* @return [PlanPhase] if the action is successful, error otherwise.
*/
fun startPlan(draftPlan: R) =
PlanPhase.start(repository, currentPhase, draftPlan).also { it.onSuccess { currentPhase = it } }

/**
* Creates a draft order resource based on the state of the [currentPhase].
*
* @return [R] if the action is successful, error otherwise.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct the return kdoc like above and other places.

*/
fun draftOrder(): Result<R> {
return OrderPhase.draft(currentPhase)
}

/**
* Starts an order phase based on the state of the [currentPhase] and [draftPlan].
*
* @return [OrderPhase] if the action is successful, error otherwise.
*/
fun startOrder(updatedDraftOrder: R) =
OrderPhase.start(repository, currentPhase, updatedDraftOrder).also {
it.onSuccess { currentPhase = it }
}

/**
* Creates a draft event resource based on the state of the [currentPhase].
*
* @return [D] if the action is successful, error otherwise.
*/
fun <D : E> draftPerform(klass: Class<in D>): Result<D> {
return PerformPhase.draft<R, D>(klass, currentPhase)
}

/**
* Starts a perform phase based on the state of the [currentPhase] and [draftPlan].
*
* @return [PerformPhase] if the action is successful, error otherwise.
*/
fun <D : E> startPerform(updatedDraftPerform: D) =
PerformPhase.start<R, D>(repository, currentPhase, updatedDraftPerform).also {
it.onSuccess { currentPhase = it }
}

companion object {

/**
* Create flow for the
* [Send Message](https://build.fhir.org/ig/HL7/cqf-recommendations/examples-activities.html#send-a-message)
* activity with the [CPGCommunicationRequest].
*
* @return ActivityFlow<CPGCommunicationRequest, CPGCommunicationEvent>
*/
fun of(
repository: Repository,
resource: CPGCommunicationRequest,
): ActivityFlow<CPGCommunicationRequest, CPGCommunicationEvent> =
ActivityFlow(repository, resource)

/**
* Create flow for the
* [Send Message](https://build.fhir.org/ig/HL7/cqf-recommendations/examples-activities.html#send-a-message)
* activity with the [CPGCommunicationEvent].
*
* @return ActivityFlow<CPGCommunicationRequest, CPGCommunicationEvent>
*/
fun of(
repository: Repository,
resource: CPGCommunicationEvent,
): ActivityFlow<CPGCommunicationRequest, CPGCommunicationEvent> =
ActivityFlow(repository, null, resource)

/**
* Create flow for the
* [Order a medication](https://build.fhir.org/ig/HL7/cqf-recommendations/examples-activities.html#order-a-medication)
* activity with the [CPGMedicationRequest].
*
* @return ActivityFlow<CPGMedicationRequest, CPGOrderMedicationEvent<*>>
*/
fun of(
repository: Repository,
resource: CPGMedicationRequest,
): ActivityFlow<CPGMedicationRequest, CPGOrderMedicationEvent<*>> =
ActivityFlow(repository, resource)

/**
* Create flow for the
* [Order a medication](https://build.fhir.org/ig/HL7/cqf-recommendations/examples-activities.html#order-a-medication)
* activity with the [CPGOrderMedicationEvent].
*
* @return ActivityFlow<CPGMedicationRequest, CPGOrderMedicationEvent<*>>
*/
fun of(
repository: Repository,
resource: CPGOrderMedicationEvent<*>,
): ActivityFlow<CPGMedicationRequest, CPGOrderMedicationEvent<*>> =
ActivityFlow(repository, null, resource)

// Collect information
// fun of(repository: Repository, resource: CPGTaskRequest) =
// ActivityFlow(repository, resource, null)

// // Order a service
// fun of(
// repository: Repository,
// resource: CPGServiceRequest,
// ): ActivityFlow<CPGServiceRequest, CPGEventForOrderService<*>> =
// ActivityFlow(repository, resource)
//
// fun of(
// repository: Repository,
// resource: CPGImmunizationRequest,
// ): ActivityFlow<CPGImmunizationRequest, CPGImmunizationEvent> = ActivityFlow(repository,
// resource)
}
}
Loading
Loading