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

Release 1.2.0 #470

Merged
merged 21 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a911749
Upgrade to Kotlin 1.9.10 and other libraries
Whathecode Oct 21, 2023
ab59504
Upgrade to detekt 1.23.1 and fix formatting violations
Whathecode Oct 21, 2023
3942964
Upgrade to Gradle 8.4
Whathecode Oct 22, 2023
e9c4fa8
Upgrade `typescript-declarations` dependencies
Whathecode Oct 22, 2023
787315c
Fail on warnings for all compilations
Whathecode Nov 5, 2023
8e84797
Export Nullable and TS test to show how to pass it as arguments
Whathecode Jun 23, 2023
fe1e933
Doc: clearer subsystem decomposition
Whathecode Aug 13, 2023
1d0d2df
Upgrade to Kotlin 1.9.21 and matching serialization
Whathecode Nov 27, 2023
e41e556
Fix: JS/TS access to snapshot properties
Whathecode Feb 18, 2024
20dba1d
Move mocha configuration to .mocharc.json
Whathecode Feb 12, 2024
ed485d6
Upgrade to JS nodenext module system
Whathecode Feb 12, 2024
4d429e4
Add package.json to JS test output
Whathecode Feb 17, 2024
307d005
Make TS wrappers and tests ES modules
Whathecode Feb 12, 2024
e058fc8
Explicit references to ambient module declaration files
Whathecode Feb 22, 2024
bdf4186
Add custom participant attribute API backwards compatibilty test
Whathecode Dec 10, 2023
5366977
Prepare `GenerateExampleRequests` for overloaded methods
Whathecode Mar 16, 2024
bb9ff20
Add `changeType` to JSON object migration builder
Whathecode Mar 16, 2024
5d3ee7c
Allow adding participants by username to recruitments
xelahalo Mar 13, 2024
b7c726e
Add `Website` primary device
xelahalo Mar 11, 2024
887b3c8
Build: upgrade to Kotlin 1.9.23 and other dependencies
Whathecode Mar 16, 2024
da53151
Bump version to 1.2.0
Whathecode Mar 16, 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
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
Loading