Skip to content

Commit

Permalink
Release 1.0.0
Browse files Browse the repository at this point in the history
Merge pull request #382 from cph-cachet/develop
  • Loading branch information
Whathecode authored Apr 4, 2022
2 parents ab54c6b + 54a03d4 commit 5abfe4b
Show file tree
Hide file tree
Showing 421 changed files with 14,125 additions and 3,910 deletions.
70 changes: 54 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ Two key **design goals** differentiate this project from similar projects:
Reference implementations [will be made available](https://github.com/cph-cachet/) (and hosted) by CACHET in the future.
- **Extensibility**: Where industry standards exist (such as [Open mHealth](https://www.openmhealth.org/)), the goal is to include them in CARP.
However, we recognize that many study-specific requirements will always require parts of the infrastructure to be modified.
Rather than having to fork the entire project and set up your own hosting, domain objects in the protocols subsystem [can be extended to describe study-specific requirements](docs/carp-common.md#extending-domain-objects).
Rather than having to fork the entire project and set up your own hosting, domain objects in the protocols subsystem [can be extended to describe study-specific requirements](docs/carp-protocols.md#extending-domain-objects).
Your custom client (e.g., smartphone application) will need to know how to interpret them, but these additions _are transparent to, and compatible with, the rest of the framework_ when using the provided [built-in serializers](#serialization).

## Table of Contents

- [Architecture](#architecture)
- [Common](docs/carp-common.md)
- [Data types](docs/carp-common.md#data-types)
- [Device descriptors](docs/carp-common.md#device-descriptors)
- [Device configurations](docs/carp-common.md#device-configurations)
- [Sampling schemes and configurations](docs/carp-common.md#sampling-schemes-and-configurations)
- [Tasks](docs/carp-common.md#tasks)
- [Triggers](docs/carp-common.md#triggers)
- [Task configurations](docs/carp-common.md#task-configurations)
- [Trigger configurations](docs/carp-common.md#trigger-configurations)
- [Protocols](docs/carp-protocols.md)
- [Domain objects](docs/carp-protocols.md#domain-objects)
- [Application services](docs/carp-protocols.md#application-services)
Expand All @@ -41,6 +41,7 @@ Two key **design goals** differentiate this project from similar projects:
- [Infrastructure helpers](#infrastructure-helpers)
- [Serialization](#serialization)
- [Request objects](#request-objects)
- [Application service versioning](#application-service-versioning)
- [Authorization](#authorization)
- [Stub classes](#stub-classes)
- [Usage](#usage)
Expand Down Expand Up @@ -95,14 +96,15 @@ which implementing infrastructures are expected to implement as remote procedure
Asynchronous communication between subsystems happens via an event bus,
which implementing infrastructures are expected to implement using a message queue which guarantees order for all `IntegrationEvent`'s sharing the same `aggregateId`.

Not all subsystems are implemented yet.
Currently, this project contains an unstable (not backwards compatible) alpha version of the protocols, studies, deployments, clients, and data subsystems.
Many changes will happen as the rest of the infrastructure is implemented.
Not all subsystems are implemented or complete yet.
Currently, this project contains a stable version of the protocols, studies, deployments, and data subsystems.
The client subsystem is still considered alpha and expected to change in the future.
The resources and analysis subsystem are envisioned later additions.

## Infrastructure helpers

Even though this library does not contain dependencies on concrete infrastructure, it does provide building blocks which greatly facilitate hosting the application services defined in this library as a distributed service and consuming them.
You are not required to use these, but they remove some of the boilerplate code you would otherwise have to write.
You are not required to use these, but they remove boilerplate code you would otherwise have to write.

### Serialization

Expand All @@ -114,7 +116,7 @@ In addition, domain objects which need to be persisted (aggregate roots) impleme
All snapshots are fully serializable to JSON, making it straightforward to store them in a document store.
But, if you prefer to use a relational database instead, you can call `consumeEvents()` to get all the modifications since the object was last stored.

Lastly, custom serializers to the default ones generated by `kotlinx.serialization` are provided for [extendable types used in study protocols](docs/carp-common.md#extending-domain-objects) (e.g., `DeviceDescriptor`).
Lastly, custom serializers to the default ones generated by `kotlinx.serialization` are provided for [extendable types used in study protocols](docs/carp-protocols.md#extending-domain-objects) (e.g., `DeviceConfiguration`).
These 'magic' serializers support deserializing extending types which are unknown at runtime, allowing you to access the base properties seamlessly.
Using the built-in serializers thus allows you to handle incoming requests and persistence of extending types you do not have available at compile time.
They are used by default in all objects that need to be serialized for data transfer or snapshot storage.
Expand All @@ -123,15 +125,25 @@ More detailed information on how this works can be found in [the documentation o

### Request objects

To help implementing remote procedure calls (RPCs), each application service has matching polymorphic serializable 'request objects'.
For example, the deployments subsystem has a sealed class [`DeploymentServiceRequest`](carp.deployments.core/src/commonMain/kotlin/dk/cachet/carp/deployments/infrastructure/DeploymentServiceRequest.kt) and each subclass represents a request to `DeploymentService`.
To help implement remote procedure calls (RPCs), each application service has matching polymorphic serializable 'request objects'.
For example, the "deployments" subsystem has a sealed class [`DeploymentServiceRequest`](carp.deployments.core/src/commonMain/kotlin/dk/cachet/carp/deployments/infrastructure/DeploymentServiceRequest.kt) and each subclass represents a request to `DeploymentService`.
Using these objects, all requests to a single application service can be handled by one endpoint using type checking.
We recommend [using a when expression](https://kotlinlang.org/docs/reference/sealed-classes.html) so that the compiler can verify whether you have handled all requests.

In addition, each request object can be executed by passing a matching application service to `invokeOn`.
This allows a centralized implementation for any incoming request object to an application service.
However, in practice you might want to perform additional actions depending on specific requests, e.g., [authorization which is currently not part of core](#authorization).

### Application service versioning

When using the default serializers for the provided request objects and integration events, you can get backwards compatible application services for free.
Each new CARP version will come with the necessary application service migration functionality for new minor API versions.
Clients that are on the same _major_ version as the backend will be able to use new hosted _minor_ versions of the API.

Each application service has a corresponding `ApplicationServiceApiMigrator`.
To get support for backwards compatible application services, you need to wire a call to `migrateRequest` into your infrastructure endpoints.
`MigratedRequest.invokeOn` can be used to execute the migrated request on the application service.

### Authorization

Currently, this library does not contain support for authorization.
Expand All @@ -143,7 +155,7 @@ In a future release we might pass authorization as a dependent service to applic
### Stub classes

Stub classes are available for the abstract domain objects defined in the common subsystem.
These can be used to write unit tests in which you are not interested in testing the behavior of specific devices, triggers, etc., but rather how they are referenced from within a study protocol or deployment.
These can be used to write unit tests in which you are not interested in testing the behavior of specific device configurations, trigger configurations, etc., but rather how they are referenced from within a study protocol or deployment.

In addition, `String` manipulation functions are available to convert type names of protocol domain objects within a JSON string to 'unknown' type names. This supports testing deserialization of domain objects unknown at runtime, e.g., as defined in an application-specific client. See [the section on serialization](#serialization) for more details.

Expand Down Expand Up @@ -207,7 +219,6 @@ val studyId: UUID = studyStatus.studyId

// Let the study use the protocol from the 'carp.protocols' example above.
val trackPatientStudy: StudyProtocol = createExampleProtocol()
val patientPhone: AnyPrimaryDeviceConfiguration = trackPatientStudy.primaryDevices.first() // "Patient's phone"
val protocolSnapshot: StudyProtocolSnapshot = trackPatientStudy.getSnapshot()
studyStatus = studyService.setProtocol( studyId, protocolSnapshot )

Expand All @@ -224,8 +235,8 @@ if ( studyStatus is StudyStatus.Configuring && studyStatus.canGoLive )
// Once the study is live, you can 'deploy' it to participant's devices. They will be invited.
if ( studyStatus.canDeployToParticipants )
{
// Create a 'participant group' with a single participant, using the "Patient's phone".
val participation = AssignParticipantDevices( participant.id, setOf( patientPhone.roleName ) )
// Create a 'participant group' with a single participant; `AssignedTo.All` assigns the "Patient's phone".
val participation = AssignedParticipantRoles( participant.id, AssignedTo.All )
val participantGroup = setOf( participation )

val groupStatus: ParticipantGroupStatus = recruitmentService.inviteNewParticipantGroup( studyId, participantGroup )
Expand All @@ -244,7 +255,7 @@ val patientPhone: Smartphone = trackPatientStudy.primaryDevices.first() as Smart
// This is called by `StudyService` when deploying a participant group.
val invitation = ParticipantInvitation(
participantId = UUID.randomUUID(),
assignedPrimaryDeviceRoleNames = setOf( patientPhone.roleName ),
assignedRoles = AssignedTo.All,
identity = AccountIdentity.fromEmailAddress( "test@test.com" ),
invitation = StudyInvitation( "Movement study", "This study tracks your movements." )
)
Expand Down Expand Up @@ -275,6 +286,33 @@ status = deploymentService.getStudyDeploymentStatus( studyDeploymentId )
val isReady = status is StudyDeploymentStatus.Running // True.
```

<a name="example-data"></a>
**carp.data**: Calls to this subsystem are abstracted away by the 'deployments' subsystem and are planned to be abstracted away by the 'clients' subsystem.
Example code which is called once a deployment is running and data is subsequently uploaded by the client.

```kotlin
val dataStreamService: DataStreamService = createDataStreamEndpoint()
val studyDeploymentId: UUID = getStudyDeploymentId() // Provided by the 'deployments' subsystem.

// This is called by the `DeploymentsService` once the deployment starts running.
val device = "Patient's phone"
val geolocation = DataStreamsConfiguration.ExpectedDataStream( device, CarpDataTypes.GEOLOCATION.type )
val stepCount = DataStreamsConfiguration.ExpectedDataStream( device, CarpDataTypes.STEP_COUNT.type )
val configuration = DataStreamsConfiguration( studyDeploymentId, setOf( geolocation, stepCount ) )
dataStreamService.openDataStreams( configuration )

// Upload data from the client.
val geolocationData = MutableDataStreamSequence<Geolocation>(
dataStream = dataStreamId<Geolocation>( studyDeploymentId, device ),
firstSequenceId = 0,
triggerIds = listOf( 0 ) // Provided by device deployment; maps to the `atStartOfStudy()` trigger.
)
val uploadData: DataStreamBatch = MutableDataStreamBatch().apply {
appendSequence( geolocationData )
}
dataStreamService.appendToDataStreams( studyDeploymentId, uploadData )
```

<a name="example-client"></a>
**carp.client**: Example initialization of a smartphone client for the participant that got invited to the study in the 'studies' code sample above:

Expand Down
100 changes: 48 additions & 52 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
// Load plugin dependencies and initialize build variables.
buildscript {
ext {
// Version used for all submodule artifacts.
// Version used for submodule artifacts.
// Snapshot publishing changes (or adds) the suffix after '-' with 'SNAPSHOT' prior to publishing.
globalVersion = '1.0.0-alpha.39'
globalVersion = '1.0.0'
clientsVersion = '1.0.0-alpha.40' // The clients subsystem is still expected to change drastically.

versions = [
// Kotlin multiplatform versions.
kotlin:'1.6.10',
serialization:'1.3.1',
coroutines:'1.5.2',
datetime:'0.3.1',
serialization:'1.3.2',
coroutines:'1.6.0',
datetime:'0.3.2',

// JVM versions.
jvmTarget:'1.8',
dokkaPlugin:'1.5.30',
dokkaPlugin:'1.6.10',
reflections:'0.10.2',

// JS versions.
nodePlugin:'3.1.1',
nodePlugin:'3.2.1',
bigJs:'6.1.1',

// DevOps versions.
detektPlugin:'1.18.1',
detektPlugin:'1.20.0-RC2',
detektVerifyImplementation:'1.2.2',
nexusPublishPlugin:'1.1.0',
apacheCommons:'2.11.0'
Expand Down Expand Up @@ -65,13 +66,10 @@ if (publishPropertiesFile.exists())

// Configure all subprojects as testable, publishable, Kotlin multiplatform projects.
// A `kotlinx.serialization` dependency is added to serialize domain models.
// A `kotlinx-datetime` dependency is added to be able to store dates in domain models.
configure( subprojects - devOpsModules ) {
version = globalVersion

repositories {
mavenCentral()
}

// Specify platforms and test frameworks to use.
apply plugin: 'kotlin-multiplatform'
apply plugin: 'kotlinx-serialization'
Expand All @@ -93,16 +91,43 @@ configure( subprojects - devOpsModules ) {
commonMain {
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:${versions.serialization}"
api "org.jetbrains.kotlinx:kotlinx-datetime:${versions.datetime}"
}
}
commonTest {
dependencies {
implementation kotlin('test')
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:${versions.coroutines}"
}
}
jvmTest {
dependencies {
implementation kotlin('reflect')
implementation "org.reflections:reflections:${versions.reflections}"
}
}

all {
def isTestSourceSet = it.name.endsWith('Test')

languageSettings {
// We do not mind being early adopters of Jetbrains APIs likely to change in the future.
optIn('kotlin.RequiresOptIn')
optIn('kotlin.time.ExperimentalTime')
if (isTestSourceSet)
{
optIn('kotlinx.coroutines.ExperimentalCoroutinesApi')
}
}
}
}
}

// Treat compilation warning as errors for all compilation targets.
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions { allWarningsAsErrors = true }
}

// Publish configuration.
// For signing and publishing to work, a 'publish.properties' file needs to be added to the root containing:
// The OpenPGP credentials to sign all artifacts:
Expand Down Expand Up @@ -213,9 +238,16 @@ task copyTestJsSources(type: Copy, dependsOn: setupTsProject) {
def importedPackages = file("$rootDir/build/js/packages_imported")
if (importedPackages.exists()) importedPackages.eachFile { it.delete() }

// Compile all subprojects for which TypeScript declarations are defined.
def projects = coreModules + commonModule
projects.each { dependsOn("${it.name}:jsBrowserDistribution") }
// Compile all subprojects which compile to JS.
// TODO: Can the compiled sources be copied from the tasks of which we want to test the output directly?
// We only need main sources of coreModules and commonModules since these are the only ones tested.
// But, only adding dependencies on those triggers warnings since other outputs exist in `/build//js/packages`.
def projects = subprojects - devOpsModules
projects.each {
def project = it.name
dependsOn("$project:jsBrowserDistribution")
dependsOn("$project:compileTestKotlinJs")
}

// Copy compiled sources and dependencies to test project node_modules.
from "$rootDir/build/js/packages"
Expand Down Expand Up @@ -247,47 +279,15 @@ task verifyTsDeclarations(type: NodeTask, dependsOn: compileTs) {
]
}

// Add common dependencies and configuration:
// - `carp.test` for testing
// - reflection for testing on JVM
// TODO: unknown polymorphic serializers are only expected to be used on the server-side. Can this feature be made optional or refactored?
// Add `carp.test` helpers.
configure( coreModules + commonModule ) {
buildscript {
repositories {
mavenCentral()
}

dependencies {
classpath "org.jetbrains.kotlin:kotlin-serialization:${versions.kotlin}"
}
}

kotlin {
sourceSets {
commonMain {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-datetime:${versions.datetime}"
}
}
commonTest {
dependencies {
implementation project(':carp.test')
}
}
jvmTest {
dependencies {
implementation kotlin('reflect')
implementation "org.reflections:reflections:${versions.reflections}"
}
}

all {
languageSettings {
// We do not mind being early adopters of Jetbrains APIs likely to change in the future.
useExperimentalAnnotation("kotlin.RequiresOptIn")
useExperimentalAnnotation("kotlin.time.ExperimentalTime")
}
}
}
}
}
Expand Down Expand Up @@ -315,10 +315,6 @@ configure( coreModules ) {
// Add code analysis.
configure( rootProject )
{
repositories {
mavenCentral()
}

apply plugin: 'io.gitlab.arturbosch.detekt'
detekt {
dependencies {
Expand Down
2 changes: 2 additions & 0 deletions carp.clients.core/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
group = 'dk.cachet.carp.clients'

version = clientsVersion

publishing {
publications {
all {
Expand Down
Loading

0 comments on commit 5abfe4b

Please sign in to comment.