Skip to content

Commit

Permalink
Release 1.2.0
Browse files Browse the repository at this point in the history
Merge pull request #470 from cph-cachet/develop
  • Loading branch information
Whathecode authored Mar 18, 2024
2 parents 6c99354 + da53151 commit 0a465e9
Show file tree
Hide file tree
Showing 113 changed files with 3,565 additions and 1,814 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,23 @@ Two key **design goals** differentiate this project from similar projects:

![Subsystem decomposition](https://i.imgur.com/hEsTHNk.png)

- [**Protocols**](docs/carp-protocols.md): Implements open standards which can describe a study protocol—how a study should be run. Essentially, this subsystem has no _technical_ dependencies on any particular sensor technology or application as it merely describes why, when, and what data should be collected.
- [**Protocols**](docs/carp-protocols.md): Supports management of study protocols—how a study should be run. Essentially, this subsystem has no _technical_ dependencies on any particular sensor technology or application as the containing study protocols merely describe why, when, and what data should be collected.

[![Maven Central](https://maven-badges.herokuapp.com/maven-central/dk.cachet.carp.protocols/carp.protocols.core/badge.svg)](https://mvnrepository.com/artifact/dk.cachet.carp.protocols) [![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/dk.cachet.carp.protocols/carp.protocols.core?server=https%3A%2F%2Foss.sonatype.org)](https://oss.sonatype.org/content/repositories/snapshots/dk/cachet/carp/protocols/)

- [**Studies**](docs/carp-studies.md): Supports management of research studies, including the recruitment of participants and assigning metadata (e.g., contact information). This subsystem maps pseudonymized data (managed by the 'deployments' subsystem) to actual participants.

[![Maven Central](https://maven-badges.herokuapp.com/maven-central/dk.cachet.carp.studies/carp.studies.core/badge.svg)](https://mvnrepository.com/artifact/dk.cachet.carp.studies) [![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/dk.cachet.carp.studies/carp.studies.core?server=https%3A%2F%2Foss.sonatype.org)](https://oss.sonatype.org/content/repositories/snapshots/dk/cachet/carp/studies/)

- [**Deployments**](docs/carp-deployments.md): Maps the information specified in a study protocol to runtime configurations used by the 'clients' subystem to run the protocol on concrete devices (e.g., a smartphone) and allow researchers to monitor their state. To start collecting data, participants need to be invited, devices need to be registered, and consent needs to be given to collect the requested data.
- [**Deployments**](docs/carp-deployments.md): Maps the information specified in a study protocol to runtime configurations used by 'client' subsystems to run the protocol on concrete devices (e.g., a smartphone) and allow researchers to monitor their state. To start collecting data, participants need to be invited, devices need to be registered, and consent needs to be given to collect the requested data.

[![Maven Central](https://maven-badges.herokuapp.com/maven-central/dk.cachet.carp.deployments/carp.deployments.core/badge.svg)](https://mvnrepository.com/artifact/dk.cachet.carp.deployments) [![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/dk.cachet.carp.deployments/carp.deployments.core?server=https%3A%2F%2Foss.sonatype.org)](https://oss.sonatype.org/content/repositories/snapshots/dk/cachet/carp/deployments/)

- [**Clients**](docs/carp-clients.md): The runtime which performs the actual data collection on a device (e.g., desktop computer or smartphone). This subsystem contains reusable components which understand the runtime configuration derived from a study protocol by the deployment subsystem. Integrations with sensors are loaded through a 'device data collector' plug-in system to decouple sensing—not part of core—from sensing logic.
- [**Clients**](docs/carp-clients.md): The runtime which performs the actual data collection on a device (e.g., desktop computer or smartphone). This subsystem contains reusable components which understand the runtime configuration derived from a study protocol by the 'deployment' subsystem. Integrations with sensors are loaded through a 'device data collector' plug-in system to decouple sensing—not part of core—from sensing logic.

[![Maven Central](https://maven-badges.herokuapp.com/maven-central/dk.cachet.carp.clients/carp.clients.core/badge.svg?color=orange)](https://mvnrepository.com/artifact/dk.cachet.carp.clients) [![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/dk.cachet.carp.clients/carp.clients.core?server=https%3A%2F%2Foss.sonatype.org)](https://oss.sonatype.org/content/repositories/snapshots/dk/cachet/carp/clients/)

- [**Data**](docs/carp-data.md): Contains all pseudonymized data. In combination with the original study protocol, the full provenance of the data (when/why it was collected) is known.
- [**Data**](docs/carp-data.md): Contains all pseudonymized data. In combination with the original study protocol, the full provenance of the data (when/why it was collected) is known. In combination with data from the 'studies' subsystem, the data can be linked back to individual participants.

[![Maven Central](https://maven-badges.herokuapp.com/maven-central/dk.cachet.carp.data/carp.data.core/badge.svg)](https://mvnrepository.com/artifact/dk.cachet.carp.data) [![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/dk.cachet.carp.data/carp.data.core?server=https%3A%2F%2Foss.sonatype.org)](https://oss.sonatype.org/content/repositories/snapshots/dk/cachet/carp/data/)

Expand Down Expand Up @@ -363,9 +363,9 @@ if ( status is StudyStatus.RegisteringDevices )

In case you want to contribute, please follow our [contribution guidelines](https://github.com/cph-cachet/carp.core-kotlin/blob/develop/CONTRIBUTING.md).

We recommend using IntelliJ IDEA 2022, as this is the development environment we use and is therefore fully tested.
We recommend using IntelliJ IDEA 2023, as this is the development environment we use and is therefore fully tested.

- Open the project folder in IntelliJ 2022.
- Open the project folder in IntelliJ 2023.
- Make sure Google Chrome is installed; JS unit tests are run on headless Chrome.
- In case you want to run TypeScript declaration tests (`verifyTsDeclarations`), install node.
- To build/test/publish, click "Edit Configurations" to add configurations for [the included Gradle tasks](#gradle-tasks), or run them from the Gradle tool window.
Expand Down
106 changes: 64 additions & 42 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,37 @@ buildscript {
ext {
// Version used for submodule artifacts.
// Snapshot publishing changes (or adds) the suffix after '-' with 'SNAPSHOT' prior to publishing.
globalVersion = '1.1.2'
clientsVersion = '1.1.2-alpha.1' // The clients subsystem is still expected to change drastically.
globalVersion = '1.2.0'
clientsVersion = '1.2.0-alpha.1' // The clients subsystem is still expected to change drastically.

versions = [
// Kotlin multiplatform versions.
kotlin:'1.8.21',
serialization:'1.5.0',
coroutines:'1.7.0',
datetime:'0.4.0',
kotlin:'1.9.23',
serialization:'1.6.3',
coroutines:'1.8.0',
datetime:'0.5.0',

// JVM versions.
jvmTarget:'1.8',
dokkaPlugin:'1.8.10',
dokkaPlugin:'1.9.20',
reflections:'0.10.2',

// JS versions.
nodePlugin:'4.0.0',
nodePlugin:'7.0.2',
bigJs:'6.2.1',

// DevOps versions.
detektPlugin:'1.22.0',
detektPlugin:'1.23.5',
detektVerifyImplementation:'1.2.5',
nexusPublishPlugin:'1.3.0',
apacheCommons:'2.11.0'
apacheCommons:'2.15.1'
]

commonModule = subprojects.find { it.name == 'carp.common' }
coreModules = subprojects.findAll { it.name.endsWith( '.core' ) }
testModules = subprojects.findAll { it.name == 'carp.common.test' || it.name == 'carp.test' }
publishNpmModule = subprojects.find { it.name == 'publish-npm-packages' }
allModules = coreModules + testModules + commonModule + publishNpmModule
devOpsModules =
subprojects.findAll {it.name == 'carp.detekt' || it.name == 'rpc' } + publishNpmModule
}
Expand Down Expand Up @@ -91,6 +93,27 @@ configure( subprojects - devOpsModules ) {
generateTypeScriptDefinitions()
}

targets.configureEach {
compilations.configureEach {
def isTestSourceSet = it.name == 'test'

compilerOptions.configure((Action) {
// Treat compilation warning as errors for all compilation targets.
it.allWarningsAsErrors = true

// We do not mind being early adopters of Jetbrains APIs likely to change in the future.
it.optIn.add('kotlin.RequiresOptIn')
it.optIn.add('kotlin.time.ExperimentalTime')
it.optIn.add('kotlin.js.ExperimentalJsExport')
if (isTestSourceSet)
{
it.optIn.add('kotlinx.coroutines.ExperimentalCoroutinesApi')
}
it.freeCompilerArgs.add('-Xexpect-actual-classes') // https://youtrack.jetbrains.com/issue/KT-61573
} )
}
}

sourceSets {
commonMain {
dependencies {
Expand All @@ -110,29 +133,9 @@ configure( subprojects - devOpsModules ) {
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')
optIn('kotlin.js.ExperimentalJsExport')
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 @@ -233,15 +236,15 @@ task setSnapshotVersion {

// TypeScript ambient declaration verification.
def typescriptFolder = 'typescript-declarations'
def npmScope = "@cachet"
apply plugin: 'com.github.node-gradle.node'
task setupTsProject(type: NpmTask) {
workingDir = file(typescriptFolder)
args = ['install']
}
task copyTestJsSources(type: Copy, dependsOn: setupTsProject) {
// Compile production sources for CARP, and the JS publication project (`publishNpmModule`).
def projects = coreModules + commonModule + publishNpmModule
projects.each {
allModules.each {
def project = it.name
dependsOn("$project:jsProductionExecutableCompileSync")
}
Expand All @@ -257,7 +260,7 @@ task copyTestJsSources(type: Copy, dependsOn: setupTsProject) {
}
eachFile { file ->
// Compiled sources have the name of the module they represent, followed by ".js" and ".d.ts".
// To be recognized by node, place them as "index.js" and "index.d.ts" in "node_modules/@cachet/<module-name>".
// To be recognized by node, place them as "index.js" and "index.d.ts" in "node_modules/<scope>/<module-name>".
def fileMatch = file.name =~ /(.+)\.(js|d\.ts)/
def moduleName = fileMatch[0][1]
def extension = fileMatch[0][2]
Expand All @@ -272,8 +275,8 @@ task copyTestJsSources(type: Copy, dependsOn: setupTsProject) {
// Modify sources to act like modules with exported named members.
file.filter { line ->
// Compiled sources refer to other modules as adjacent .js source files.
// Change these to the named modules created in the previous step.
def namedModules = line.replaceAll(~/'\.\/(.+?)\.js'/, "'@cachet/\$1'")
// Change these to the scoped modules created in the previous step.
def namedModules = line.replaceAll(~/'\.\/(.+?)\.js'/, "'$npmScope/\$1'")

// Replace `any` types with actual types for which facades are specified.
def replacedTypes = knownFacadeTypes.inject(namedModules) { curLine, type ->
Expand All @@ -298,9 +301,33 @@ task copyTestJsSources(type: Copy, dependsOn: setupTsProject) {
additionalExports
}
}
into "./$typescriptFolder/node_modules/@cachet/"
into "./$typescriptFolder/node_modules/$npmScope/"
}
task packageTestJsSources(type: Copy, dependsOn: copyTestJsSources) {
allModules.each {
def project = it.name
dependsOn("$project:jsPackageJson")
dependsOn("$project:jsTestPackageJson")
}

from("$rootDir/build/js/packages") {
include "**/package.json"
includeEmptyDirs = false
}
eachFile { file ->
def moduleName = file.getFile().getParentFile().name
file.filter { line ->
// Add scope to module name.
def changedName = line.replaceAll(~/("name": ).*/, "\$1 \"$npmScope/$moduleName\",")

// Point main source to 'index.js'.
changedName.replaceAll(~/("main": ).*/, "\$1 \"index.js\",")
}

}
into "./$typescriptFolder/node_modules/$npmScope/"
}
task compileTs(type: NpmTask, dependsOn: copyTestJsSources) {
task compileTs(type: NpmTask, dependsOn: packageTestJsSources) {
workingDir = file(typescriptFolder)
args = ['run', 'tsc']
}
Expand All @@ -309,11 +336,6 @@ task verifyTsDeclarations(type: NodeTask, dependsOn: compileTs) {
execOverrides {
it.workingDir = typescriptFolder
}
args = [
'--require', 'ts-node/register',
'--require', 'jsdom-global/register',
'./tests/**/*.ts'
]
}

// Add `carp.test` helpers.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
@file:Suppress( "NON_EXPORTABLE_TYPE" )

package dk.cachet.carp.common.application.devices

import dk.cachet.carp.common.application.Trilean
import dk.cachet.carp.common.application.data.DataType
import dk.cachet.carp.common.application.sampling.DataTypeSamplingSchemeMap
import dk.cachet.carp.common.application.sampling.SamplingConfiguration
import dk.cachet.carp.common.application.tasks.TaskConfigurationList
import dk.cachet.carp.common.infrastructure.serialization.NotSerializable
import kotlinx.serialization.Required
import kotlinx.serialization.Serializable
import kotlin.js.JsExport
import kotlin.reflect.KClass


/**
* A website which participates in a study as a primary device.
*/
@Serializable
@JsExport
data class Website(
override val roleName: String,
override val isOptional: Boolean = false
) : PrimaryDeviceConfiguration<WebsiteDeviceRegistration, WebsiteDeviceRegistrationBuilder>()
{
object Sensors : DataTypeSamplingSchemeMap()
object Tasks : TaskConfigurationList()

override fun getSupportedDataTypes(): Set<DataType> = Sensors.keys
override fun getDataTypeSamplingSchemes(): DataTypeSamplingSchemeMap = Sensors

override val defaultSamplingConfiguration: Map<DataType, SamplingConfiguration> = emptyMap()

override fun createDeviceRegistrationBuilder(): WebsiteDeviceRegistrationBuilder =
WebsiteDeviceRegistrationBuilder()
override fun getRegistrationClass(): KClass<WebsiteDeviceRegistration> = WebsiteDeviceRegistration::class
override fun isValidRegistration( registration: WebsiteDeviceRegistration ): Trilean = Trilean.TRUE
}


/**
* A [DeviceRegistration] for a [Website], specifying the [url] where the study runs.
*/
@Serializable
@JsExport
data class WebsiteDeviceRegistration(
val url: String,
/**
* The HTTP User-Agent header of the user agent which made the HTTP request to [url].
*/
val userAgent: String,
@Required
override val deviceDisplayName: String? = userAgent
) : DeviceRegistration()
{
@Required
override val deviceId: String = url
}


@Suppress( "SERIALIZER_TYPE_INCOMPATIBLE" )
@Serializable( NotSerializable::class )
@JsExport
class WebsiteDeviceRegistrationBuilder : DeviceRegistrationBuilder<WebsiteDeviceRegistration>()
{
/**
* The web URL from which the [Website] is accessed.
*/
var url: String = ""

/**
* The HTTP User-Agent header of the user agent which made the HTTP request to [url].
*/
var userAgent: String = ""

override fun build(): WebsiteDeviceRegistration = WebsiteDeviceRegistration( url, userAgent, deviceDisplayName )
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ class EventSubscriptionBuilder(
*/
inline fun <
reified TService : ApplicationService<TService, TEvent>,
reified TEvent : IntegrationEvent<TService>> event(
reified TEvent : IntegrationEvent<TService>
> event(
noinline handler: suspend (TEvent) -> Unit
) = eventBus.registerHandler( TService::class, TEvent::class, subscriber, handler )
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ abstract class EventBus
*/
fun <
TService : ApplicationService<TService, TEvent>,
TEvent : IntegrationEvent<TService>> registerHandler(
TEvent : IntegrationEvent<TService>
> registerHandler(
eventSource: KClass<TService>,
eventType: KClass<TEvent>,
subscriber: Any,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ data class WebTask(
/**
* Identifies the condition, defined by the study protocol, which caused the [WebTask] to be triggered.
*/
TRIGGER_ID( markup( "trigger id" ) );
TRIGGER_ID( markup( "trigger id" ) )
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@ import dk.cachet.carp.common.application.Immutable
import dk.cachet.carp.common.application.ImplementAsDataClass
import dk.cachet.carp.common.application.UUID
import kotlinx.datetime.Instant
import kotlin.js.JsExport


/**
* An immutable snapshot of an [AggregateRoot] at a given moment in time.
*/
@Immutable
@ImplementAsDataClass
@JsExport
interface Snapshot<TAggregateRoot>
{
val id: UUID

/**
* The date when the object represented by this snapshot was created.
*/
@Suppress( "NON_EXPORTABLE_TYPE" )
val createdOn: Instant

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,7 @@ val COMMON_SERIAL_MODULE = SerializersModule {
subclass( HeartRate::class )
subclass( InterbeatInterval::class )
subclass( NonGravitationalAcceleration::class )
// HACK: explicit serializer needs to be registered for object declarations due to limitation of the JS legacy backend.
// https://github.com/Kotlin/kotlinx.serialization/issues/1138#issuecomment-707989920
// This can likely be removed once we upgrade to the new IR backend.
subclass( NoData::class, NoData.serializer() )
subclass( NoData::class )
subclass( PPG::class )
subclass( SignalStrength::class )
subclass( SensorSkinContact::class )
Expand Down Expand Up @@ -63,6 +60,7 @@ val COMMON_SERIAL_MODULE = SerializersModule {
{
subclass( CustomProtocolDevice::class )
subclass( Smartphone::class )
subclass( Website::class )

subclass( CustomPrimaryDeviceConfiguration::class )
}
Expand All @@ -87,6 +85,7 @@ val COMMON_SERIAL_MODULE = SerializersModule {
subclass( BLESerialNumberDeviceRegistration::class )
subclass( DefaultDeviceRegistration::class )
subclass( MACAddressDeviceRegistration::class )
subclass( WebsiteDeviceRegistration::class )

subclass( CustomDeviceRegistration::class )
defaultDeserializer { DeviceRegistrationSerializer }
Expand Down
Loading

0 comments on commit 0a465e9

Please sign in to comment.