Skip to content

Commit 6c99354

Browse files
authored
Release 1.1.2
Merge pull request #442 from cph-cachet/develop
2 parents cbda553 + aea801b commit 6c99354

File tree

229 files changed

+3947
-5338
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

229 files changed

+3947
-5338
lines changed

README.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ CARP Core is a software framework to help developers build research platforms to
66
It provides modules to define, deploy, and monitor research studies, and to collect data from multiple devices at multiple locations.
77

88
It is the result of a collaboration between [iMotions](https://imotions.com/) and the [Copenhagen Center for Health Technology (CACHET)](https://www.cachet.dk/).
9-
Both use CARP Core to implement their respective research platforms: the [iMotions Mobile Research Platform](https://imotions.com/mobile-platform-landing-page-submissions/) and the [Copenhagen Research Platform (CARP)](https://carp.cachet.dk/).
10-
CARP Core is now maintained fully by iMotions (since 1.0), but [still part of CARP](https://carp.cachet.dk/core/) as an ongoing collaboration.
9+
Both use CARP Core to implement their respective research platforms: the [iMotions Mobile Research Platform](https://imotions.com/products/imotions-mobile/) and the [Copenhagen Research Platform (CARP)](https://carp.cachet.dk/).
1110

1211
Following [domain-driven design](https://en.wikipedia.org/wiki/Domain-driven_design), this project contains all domain models and application services for all CARP subsystems ([depicted below](#architecture)), not having any dependencies on concrete infrastructure.
1312
As such, this project defines an **open standard for distributed data collection**, [available for Kotlin, the Java runtime, and JavaScript](#usage), which others can build upon to create their own infrastructure.
@@ -52,8 +51,9 @@ Two key **design goals** differentiate this project from similar projects:
5251
- [Stub classes](#stub-classes)
5352
- [Usage](#usage)
5453
- [Example](#example)
55-
- [Building the project](#building-the-project)
54+
- [Development](#development)
5655
- [Gradle tasks](#gradle-tasks)
56+
- [Release management](#release-management)
5757
- [Development checklists](#development-checklists)
5858

5959
## Architecture
@@ -192,7 +192,7 @@ val ownerId = UUID.randomUUID()
192192
val protocol = StudyProtocol( ownerId, "Track patient movement" )
193193

194194
// Define which devices are used for data collection.
195-
val phone = Smartphone( "Patient's phone" )
195+
val phone = Smartphone.create( "Patient's phone" )
196196
{
197197
// Configure device-specific options, e.g., frequency to collect data at.
198198
defaultSamplingConfiguration {
@@ -359,7 +359,7 @@ if ( status is StudyStatus.RegisteringDevices )
359359
}
360360
```
361361

362-
## Building the project
362+
## Development
363363

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

@@ -384,6 +384,29 @@ For `carp.core-kotlin`:
384384
Preface with `setSnapshotVersion` task to publish to the snapshot repository, substituting the suffix of the version specified in `ext.globalVersion` with `-SNAPSHOT`.
385385
See main `build.gradle` for details.
386386

387+
### Release management
388+
389+
[Semantic versioning](https://semver.org/) is used for releases.
390+
Backwards compatibility is assessed from the perspective of clients using an implementation of the framework,
391+
as opposed to developers using the framework to implement an infrastructure.
392+
In other words, versioning is based on the exposed API (`application` namespaces), but not the domain used to implement infrastructures (`domain` namespaces).
393+
Breaking changes between `minor` versions can occur in domain objects, including the need to do database migrations.
394+
395+
Module versions are configured in the main `build.gradle` in `ext.globalVersion` and `ext.clientsVersion`.
396+
397+
Workflows:
398+
- Each push to `develop` triggers a snapshot release of the currently configured version.
399+
- Each push to `master` triggers a release to Maven using the currently configured version.
400+
401+
Releases require a couple of manual steps:
402+
- Before merging into `master`, make sure new versions are set in `build.gradle`.
403+
This should be done already in the last step, but you may decide to make a bigger version increment.
404+
- Merge into master; **don't rebase**. Rebasing causes branch commit histories to diverge which complicates later releases and messes up the visible commit history with duplicate commits.
405+
- Create a release tag on `master` with release notes.
406+
- Add `javascript-typescript-sources.zip` and `rpc-examples.zip` assets to release.
407+
This should be automated in the future: [#371](https://github.com/cph-cachet/carp.core-kotlin/issues/371) and [#416](https://github.com/cph-cachet/carp.core-kotlin/issues/416) respectively.
408+
- Bump versions on `develop` so that snapshot releases target the next version.
409+
387410
### Development checklists
388411

389412
When changes are made to CARP Core, various parts in the codebase sometimes need to be updated accordingly.

build.gradle

Lines changed: 70 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,37 @@ buildscript {
33
ext {
44
// Version used for submodule artifacts.
55
// Snapshot publishing changes (or adds) the suffix after '-' with 'SNAPSHOT' prior to publishing.
6-
globalVersion = '1.1.1'
7-
clientsVersion = '1.1.1-alpha.1' // The clients subsystem is still expected to change drastically.
6+
globalVersion = '1.1.2'
7+
clientsVersion = '1.1.2-alpha.1' // The clients subsystem is still expected to change drastically.
88

99
versions = [
1010
// Kotlin multiplatform versions.
11-
kotlin:'1.8.0',
12-
serialization:'1.4.1',
13-
coroutines:'1.6.4',
11+
kotlin:'1.8.21',
12+
serialization:'1.5.0',
13+
coroutines:'1.7.0',
1414
datetime:'0.4.0',
1515

1616
// JVM versions.
1717
jvmTarget:'1.8',
18-
dokkaPlugin:'1.7.20',
18+
dokkaPlugin:'1.8.10',
1919
reflections:'0.10.2',
2020

2121
// JS versions.
22-
nodePlugin:'3.5.0',
22+
nodePlugin:'4.0.0',
2323
bigJs:'6.2.1',
2424

2525
// DevOps versions.
2626
detektPlugin:'1.22.0',
2727
detektVerifyImplementation:'1.2.5',
28-
nexusPublishPlugin:'1.1.0',
28+
nexusPublishPlugin:'1.3.0',
2929
apacheCommons:'2.11.0'
3030
]
3131

3232
commonModule = subprojects.find { it.name == 'carp.common' }
3333
coreModules = subprojects.findAll { it.name.endsWith( '.core' ) }
34-
devOpsModules = subprojects.findAll { it.name == 'carp.detekt' || it.name == 'rpc' }
34+
publishNpmModule = subprojects.find { it.name == 'publish-npm-packages' }
35+
devOpsModules =
36+
subprojects.findAll {it.name == 'carp.detekt' || it.name == 'rpc' } + publishNpmModule
3537
}
3638

3739
dependencies {
@@ -82,9 +84,11 @@ configure( subprojects - devOpsModules ) {
8284
useJUnitPlatform()
8385
}
8486
}
85-
js(LEGACY) {
86-
binaries.executable()
87+
js(IR) {
88+
moduleName = project.name.replaceAll("\\.", "-") + "-generated"
89+
binaries.executable() // Export JS/TypeScript files.
8790
browser()
91+
generateTypeScriptDefinitions()
8892
}
8993

9094
sourceSets {
@@ -114,6 +118,7 @@ configure( subprojects - devOpsModules ) {
114118
// We do not mind being early adopters of Jetbrains APIs likely to change in the future.
115119
optIn('kotlin.RequiresOptIn')
116120
optIn('kotlin.time.ExperimentalTime')
121+
optIn('kotlin.js.ExperimentalJsExport')
117122
if (isTestSourceSet)
118123
{
119124
optIn('kotlinx.coroutines.ExperimentalCoroutinesApi')
@@ -234,41 +239,73 @@ task setupTsProject(type: NpmTask) {
234239
args = ['install']
235240
}
236241
task copyTestJsSources(type: Copy, dependsOn: setupTsProject) {
237-
// Make sure no old imported packages are left behind.
238-
def importedPackages = file("$rootDir/build/js/packages_imported")
239-
if (importedPackages.exists()) importedPackages.eachFile { it.delete() }
240-
241-
// Compile all subprojects which compile to JS.
242-
// TODO: Can the compiled sources be copied from the tasks of which we want to test the output directly?
243-
// We only need main sources of coreModules and commonModules since these are the only ones tested.
244-
// But, only adding dependencies on those triggers warnings since other outputs exist in `/build//js/packages`.
245-
def projects = subprojects - devOpsModules
242+
// Compile production sources for CARP, and the JS publication project (`publishNpmModule`).
243+
def projects = coreModules + commonModule + publishNpmModule
246244
projects.each {
247245
def project = it.name
248-
dependsOn("$project:jsBrowserDistribution")
249-
dependsOn("$project:compileTestKotlinJs")
246+
dependsOn("$project:jsProductionExecutableCompileSync")
250247
}
251248

252-
// Copy compiled sources and dependencies to test project node_modules.
253-
from "$rootDir/build/js/packages"
254-
exclude '**/node_modules/**'
255-
from(importedPackages) {
256-
eachFile {
257-
def path = it.path
258-
if (path == ".visited") return // We don't need this file.
249+
// Copy compiled JS and TypeScript sources to test project's node_modules.
250+
from("$rootDir/build/js/packages/publish-npm-packages-generated") {
251+
include "**/*.js"
252+
}
253+
from("$rootDir/build/js/packages") {
254+
// Use individually generated TypeScript declarations to exclude publish-npm-packages exports.
255+
include "**/*.d.ts"
256+
includeEmptyDirs = false
257+
}
258+
eachFile { file ->
259+
// Compiled sources have the name of the module they represent, followed by ".js" and ".d.ts".
260+
// To be recognized by node, place them as "index.js" and "index.d.ts" in "node_modules/@cachet/<module-name>".
261+
def fileMatch = file.name =~ /(.+)\.(js|d\.ts)/
262+
def moduleName = fileMatch[0][1]
263+
def extension = fileMatch[0][2]
264+
file.relativePath = new RelativePath(true, moduleName, "index.$extension")
265+
266+
// Non-exported types show up as `any/* some.unknown.Type */` in generated TypeScript sources.
267+
// Types for which a facade has been manually added can be replaced with the actual type (instead of `any`).
268+
def knownFacadeTypes = []
269+
def knownFacadeTypesFile = new File("$rootDir/publish-npm-packages/src/known-facade-types")
270+
knownFacadeTypesFile.eachLine { type -> knownFacadeTypes << type }
271+
272+
// Modify sources to act like modules with exported named members.
273+
file.filter { line ->
274+
// Compiled sources refer to other modules as adjacent .js source files.
275+
// Change these to the named modules created in the previous step.
276+
def namedModules = line.replaceAll(~/'\.\/(.+?)\.js'/, "'@cachet/\$1'")
259277

260-
// Remove intermediate version directory: e.g. "kotlin/1.5.10/kotlin.js"
261-
it.path = path.replaceFirst(/\d+\.\d+.\d+(-.+)?\//, "")
278+
// Replace `any` types with actual types for which facades are specified.
279+
def replacedTypes = knownFacadeTypes.inject(namedModules) { curLine, type ->
280+
def knownType = curLine.replaceAll(
281+
~/any\/\* $type(<.+?>)? \*\//,
282+
"$type\$1"
283+
)
284+
knownType.replaceAll(~/UnknownType \*/, "any")
285+
}
286+
287+
// Add additional internal types to be exported, as configured in `forced-exports`.
288+
def toExport = []
289+
def forcedExportsFile = new File("$rootDir/publish-npm-packages/src/forced-exports/$moduleName")
290+
if (forcedExportsFile.exists()) {
291+
forcedExportsFile.eachLine { type -> toExport << type }
292+
}
293+
def toExportList = toExport.collect { "_.\\\$_\\\$.$it = $it\n " }
294+
def additionalExports = replacedTypes.replaceAll(
295+
~/return \_;/,
296+
toExportList.join() + "return _;"
297+
)
298+
additionalExports
262299
}
263300
}
264-
into "./$typescriptFolder/node_modules"
301+
into "./$typescriptFolder/node_modules/@cachet/"
265302
}
266303
task compileTs(type: NpmTask, dependsOn: copyTestJsSources) {
267304
workingDir = file(typescriptFolder)
268305
args = ['run', 'tsc']
269306
}
270307
task verifyTsDeclarations(type: NodeTask, dependsOn: compileTs) {
271-
script = file("${typescriptFolder}/node_modules/mocha/bin/mocha")
308+
script = file("${typescriptFolder}/node_modules/mocha/bin/mocha.js")
272309
execOverrides {
273310
it.workingDir = typescriptFolder
274311
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package dk.cachet.carp.common.test.infrastructure
2+
3+
import dk.cachet.carp.common.application.services.ApplicationService
4+
import dk.cachet.carp.common.application.services.IntegrationEvent
5+
import dk.cachet.carp.common.infrastructure.services.*
6+
import kotlinx.coroutines.ExperimentalCoroutinesApi
7+
import kotlinx.coroutines.test.runTest
8+
import kotlin.test.Test
9+
import kotlin.test.assertTrue
10+
11+
12+
/**
13+
* Base class to test whether an application service decorator correctly invokes the decorated service.
14+
*/
15+
@ExperimentalCoroutinesApi
16+
@Suppress(
17+
"FunctionName",
18+
"UnnecessaryAbstractClass" // Prevent test being picked up by test runner.
19+
)
20+
abstract class ApplicationServiceDecoratorTest<
21+
TService : ApplicationService<TService, TEvent>,
22+
TEvent : IntegrationEvent<TService>,
23+
TRequest : ApplicationServiceRequest<TService, *>
24+
>(
25+
private val requestsTest: ApplicationServiceRequestsTest<TService, TRequest>,
26+
private val serviceInvoker: ApplicationServiceInvoker<TService, TRequest>
27+
)
28+
{
29+
@Test
30+
fun request_invoker_calls_service() = runTest {
31+
// Create logged service.
32+
val service = requestsTest.createService()
33+
val logger = ApplicationServiceLogger<TService, TEvent>()
34+
val eventBusLog = EventBusLog( SingleThreadedEventBus() ) // Ignore events.
35+
val ignoreServiceInvocation =
36+
object : Command<TRequest>
37+
{
38+
// The returned result goes unused in this test, so just return null.
39+
override suspend fun invoke( request: TRequest ): Any? = null
40+
}
41+
val loggedService = requestsTest.decoratedServiceConstructor( service )
42+
{ ApplicationServiceRequestLogger( eventBusLog, logger::addLog, ignoreServiceInvocation ) }
43+
44+
// Test whether each invoked method on the decorated service is converted back into the same request object.
45+
// `requestTest` guarantees a request for each call is available.
46+
requestsTest.requests.forEach {
47+
serviceInvoker.invokeOnService( it, loggedService )
48+
assertTrue(
49+
logger.wasCalled( it ),
50+
"Service wasn't called or parameters of called request don't match: $it"
51+
)
52+
logger.clear()
53+
}
54+
}
55+
}

carp.common.test/src/commonMain/kotlin/dk/cachet/carp/common/test/infrastructure/ApplicationServiceRequestsTest.kt

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,30 @@
11
package dk.cachet.carp.common.test.infrastructure
22

33
import dk.cachet.carp.common.application.services.ApplicationService
4-
import dk.cachet.carp.common.infrastructure.services.ApplicationServiceLoggingProxy
5-
import dk.cachet.carp.common.infrastructure.services.ApplicationServiceRequest
4+
import dk.cachet.carp.common.infrastructure.services.*
65
import dk.cachet.carp.common.infrastructure.test.createTestJSON
76
import kotlinx.coroutines.ExperimentalCoroutinesApi
8-
import kotlinx.coroutines.test.runTest
97
import kotlinx.serialization.*
108
import kotlinx.serialization.descriptors.*
119
import kotlinx.serialization.json.*
1210
import kotlin.test.*
1311

1412

1513
/**
16-
* Base class to test whether application service request objects can be serialized,
17-
* and whether they correctly call the application service on invoke.
14+
* Base class to test whether application service request objects can be serialized.
1815
*/
1916
@ExperimentalCoroutinesApi
2017
@Suppress( "FunctionName" )
2118
abstract class ApplicationServiceRequestsTest<
2219
TService : ApplicationService<TService, *>,
2320
TRequest : ApplicationServiceRequest<TService, *>
2421
>(
22+
val decoratedServiceConstructor: (TService, (Command<TRequest>) -> Command<TRequest>) -> TService,
2523
private val requestSerializer: KSerializer<TRequest>,
26-
private val requests: List<TRequest>
24+
val requests: List<TRequest>
2725
)
2826
{
29-
abstract fun createServiceLoggingProxy(): ApplicationServiceLoggingProxy<TService, *>
27+
abstract fun createService(): TService
3028

3129

3230
@ExperimentalSerializationApi
@@ -42,20 +40,6 @@ abstract class ApplicationServiceRequestsTest<
4240
assertEquals( allRequestObjects, testedRequestObjects )
4341
}
4442

45-
@Suppress( "UNCHECKED_CAST" )
46-
@Test
47-
fun invokeOn_requests_call_service() = runTest {
48-
val serviceLog = createServiceLoggingProxy()
49-
50-
requests.forEach { request ->
51-
try { request.invokeOn( serviceLog as TService ) }
52-
catch ( ignore: Exception ) { } // Requests do not have to succeed to verify request arrived.
53-
assertTrue( serviceLog.wasCalled( request ) )
54-
55-
serviceLog.clear()
56-
}
57-
}
58-
5943
@Test
6044
fun can_serialize_and_deserialize_requests()
6145
{

carp.common.test/src/jvmMain/kotlin/dk/cachet/carp/common/test/infrastructure/versioning/BackwardsCompatibilityTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ abstract class BackwardsCompatibilityTest<TService : ApplicationService<TService
118118
private suspend fun replayLoggedRequests( fileName: String, loggedRequests: List<LoggedJsonRequest> )
119119
{
120120
val (service, eventBus) = createService()
121-
val apiMigrator = serviceInfo.apiMigrator as ApplicationServiceApiMigrator<TService>
121+
val apiMigrator = serviceInfo.apiMigrator as ApplicationServiceApiMigrator<TService, *>
122122

123123
loggedRequests.forEachIndexed { index, logged ->
124124
val replayErrorBase = "Couldn't replay requests in: $fileName. Request #${index + 1}"

0 commit comments

Comments
 (0)