Skip to content

Commit

Permalink
Add ApplicationData datatype
Browse files Browse the repository at this point in the history
  • Loading branch information
yuanchen233 authored and Whathecode committed Oct 5, 2024
1 parent f4c3994 commit 28d5e3a
Show file tree
Hide file tree
Showing 19 changed files with 77 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package dk.cachet.carp.common.application

import dk.cachet.carp.common.infrastructure.serialization.ApplicationDataSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import kotlin.js.JsExport

/**
* A [String] data holder that can store application-specific data.
* It includes a custom serializer that serializes the [String] as a [JsonElement], but only when a JSON encoder/decoder is used.
*
* This is useful for storing application-specific data that is not statically known to the common base infrastructure
* when JSON serialization is applied, without needing to escape the JSON data.
*
* If the JSON contained in the string is malformed, it will be serialized as a normal, escaped string.
*/

@Serializable( with = ApplicationDataSerializer::class )
@JsExport
data class ApplicationData(
val data: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import dk.cachet.carp.common.application.ImplementAsDataClass
import dk.cachet.carp.common.infrastructure.serialization.NotSerializable
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.*
import kotlinx.serialization.Polymorphic
import kotlinx.serialization.Required
import kotlinx.serialization.Serializable
import kotlin.js.JsExport


Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dk.cachet.carp.common.infrastructure.serialization

import dk.cachet.carp.common.application.ApplicationData
import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import kotlinx.serialization.descriptors.*
Expand All @@ -8,39 +9,41 @@ import kotlinx.serialization.json.*


/**
* Serializes [String] as a [JsonElement], but only in case a JSON encoder/decoder is used.
* Serializes [ApplicationData] as a [JsonElement], but only in case a JSON encoder/decoder is used.
*
* This is useful to store application-specific data which is not statically known to a common base infrastructure
* when JSON serialization is used, without having to escape the JSON data.
*
* In case the JSON contained in the String is malformed, it will be serialized as a normal escaped string.
*/
@OptIn( ExperimentalSerializationApi::class )
class ApplicationDataSerializer : KSerializer<String?>
class ApplicationDataSerializer : KSerializer<ApplicationData?>
{
override val descriptor: SerialDescriptor = String.serializer().nullable.descriptor

override fun deserialize( decoder: Decoder ): String?
override fun deserialize( decoder: Decoder ): ApplicationData?
{
// Early out when the value is null.
if ( !decoder.decodeNotNullMark() ) return null

// Application data is only serialized as JSON for JSON encoder.
if ( decoder !is JsonDecoder ) return decoder.decodeNullableSerializableValue( String.serializer().nullable )
if ( decoder !is JsonDecoder ) return ApplicationData( decoder.decodeSerializableValue( String.serializer() ) )

// Read application data which is stored as JSON.
val jsonElement = decoder.decodeJsonElement()
val originalString = jsonElement.toString()

// In case application data was a primitive string, trim the surrounding quotes.
return if ( jsonElement is JsonObject ) originalString
return ApplicationData(
if ( jsonElement is JsonObject ) originalString
else originalString.substring( 1, originalString.length - 1 )
)
}

override fun serialize( encoder: Encoder, value: String? )
override fun serialize( encoder: Encoder, value: ApplicationData? )
{
// Early out when the value is null.
if ( value == null )
if (value?.data == null)
{
encoder.encodeNull()
return
Expand All @@ -49,17 +52,17 @@ class ApplicationDataSerializer : KSerializer<String?>
// Application data is only serialized as JSON for JSON encoder.
if ( encoder !is JsonEncoder )
{
encoder.encodeNullableSerializableValue( String.serializer().nullable, value )
encoder.encodeNullableSerializableValue( String.serializer().nullable, value.data )
return
}

val json = encoder.json
var isJsonObject = value.startsWith( "{" )
var isJsonObject = value.data.startsWith( "{" )
if ( isJsonObject )
{
try
{
val jsonElement = json.parseToJsonElement( value )
val jsonElement = json.parseToJsonElement( value.data )
encoder.encodeJsonElement( jsonElement )
}
catch( _: SerializationException )
Expand All @@ -68,6 +71,6 @@ class ApplicationDataSerializer : KSerializer<String?>
}
}

if ( !isJsonObject ) encoder.encodeString( value )
if ( !isJsonObject ) encoder.encodeString( value.data )
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dk.cachet.carp.common.infrastructure.serialization

import dk.cachet.carp.common.application.ApplicationData
import kotlinx.serialization.*
import kotlin.test.*

Expand All @@ -12,8 +13,7 @@ class ApplicationDataSerializerTest
@Serializable
data class ContainsApplicationData(
val normalData: String,
@Serializable( ApplicationDataSerializer::class )
val applicationData: String?
val applicationData: ApplicationData?
)

private val json = createDefaultJSON()
Expand All @@ -22,7 +22,7 @@ class ApplicationDataSerializerTest
@Test
fun can_serialize_and_deserialize_non_json_application_data()
{
val toSerialize = ContainsApplicationData( "normal", "some application data" )
val toSerialize = ContainsApplicationData( "normal", ApplicationData( "some application data" ) )

val serialized = json.encodeToString( toSerialize )
val parsed: ContainsApplicationData = json.decodeFromString( serialized )
Expand All @@ -43,7 +43,7 @@ class ApplicationDataSerializerTest
}
"""
)
val toSerialize = ContainsApplicationData( "normal", applicationData.toString() )
val toSerialize = ContainsApplicationData( "normal", ApplicationData( applicationData.toString() ) )

val serialized = json.encodeToString( toSerialize )
val parsed: ContainsApplicationData = json.decodeFromString( serialized )
Expand All @@ -63,7 +63,7 @@ class ApplicationDataSerializerTest
@Test
fun json_serializer_serializes_as_json_element()
{
val toSerialize = ContainsApplicationData( "normal", """{"json":"data"}""" )
val toSerialize = ContainsApplicationData( "normal", ApplicationData( """{"json":"data"}""" ) )

val serialized = json.encodeToString( toSerialize )

Expand All @@ -75,7 +75,7 @@ class ApplicationDataSerializerTest
fun can_serialize_malformed_json()
{
val malformedJson = """{"json object":"or not?"""
val toSerialize = ContainsApplicationData( "normal", malformedJson )
val toSerialize = ContainsApplicationData( "normal", ApplicationData( malformedJson ) )

val serialized = json.encodeToString( toSerialize )

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package dk.cachet.carp.common.infrastructure.serialization

import dk.cachet.carp.common.application.devices.DefaultDeviceRegistration
import dk.cachet.carp.common.application.devices.DeviceRegistration
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlin.test.*

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

package dk.cachet.carp.deployments.application

import dk.cachet.carp.common.application.ApplicationData
import dk.cachet.carp.common.application.data.DataType
import dk.cachet.carp.common.application.devices.AnyDeviceConfiguration
import dk.cachet.carp.common.application.devices.AnyPrimaryDeviceConfiguration
Expand All @@ -13,10 +14,9 @@ import dk.cachet.carp.common.application.triggers.TaskControl
import dk.cachet.carp.common.application.triggers.TriggerConfiguration
import dk.cachet.carp.common.application.users.ExpectedParticipantData
import dk.cachet.carp.common.application.users.hasNoConflicts
import dk.cachet.carp.common.infrastructure.serialization.ApplicationDataSerializer
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.*
import kotlinx.serialization.Serializable
import kotlin.js.JsExport


Expand Down Expand Up @@ -64,8 +64,7 @@ data class PrimaryDeviceDeployment(
* This can be used by infrastructures or concrete applications which require exchanging additional data
* between the protocols and clients subsystems, outside of scope or not yet supported by CARP core.
*/
@Serializable( ApplicationDataSerializer::class )
val applicationData: String? = null
val applicationData: ApplicationData? = null
)
{
init { expectedParticipantData.hasNoConflicts( exceptionOnConflict = true ) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package dk.cachet.carp.deployments.application.users

import dk.cachet.carp.common.infrastructure.serialization.ApplicationDataSerializer
import dk.cachet.carp.common.application.ApplicationData
import kotlinx.serialization.*
import kotlin.js.JsExport

Expand All @@ -25,6 +25,5 @@ data class StudyInvitation(
* This can be used by infrastructures or concrete applications which require exchanging additional data
* between the studies and clients subsystems, outside of scope or not yet supported by CARP core.
*/
@Serializable( ApplicationDataSerializer::class )
val applicationData: String? = null
val applicationData: ApplicationData? = null
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dk.cachet.carp.deployments.application

import dk.cachet.carp.common.application.ApplicationData
import dk.cachet.carp.common.application.UUID
import dk.cachet.carp.common.application.data.input.CarpInputDataTypes
import dk.cachet.carp.common.application.data.input.Sex
Expand Down Expand Up @@ -56,7 +57,7 @@ interface ParticipationServiceTest
val (participationService, deploymentService, accountService) = createSUT()
val protocol = createSinglePrimaryDeviceProtocol()
val identity = AccountIdentity.fromEmailAddress( "[email protected]" )
val invitation = StudyInvitation( "Test study", "description", "Custom data" )
val invitation = StudyInvitation( "Test study", "description", ApplicationData( "Custom data" ) )
val participantInvitation = ParticipantInvitation(
participantId = UUID.randomUUID(),
AssignedTo.All,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

package dk.cachet.carp.deployments.domain

import dk.cachet.carp.common.application.ApplicationData
import dk.cachet.carp.common.application.UUID
import dk.cachet.carp.common.application.data.CarpDataTypes
import dk.cachet.carp.common.application.data.DataType
Expand Down Expand Up @@ -577,7 +578,7 @@ class StudyDeploymentTest
fun getDeviceDeploymentFor_succeeds()
{
val (protocol, primary, connected) = createSinglePrimaryWithConnectedDeviceProtocol()
protocol.applicationData = "some data"
protocol.applicationData = ApplicationData( "some data" )
val primaryTask = StubTaskConfiguration( "Primary task" )
val connectedTask = StubTaskConfiguration( "Connected task" )
protocol.addTaskControl( primary.atStartOfStudy().start( primaryTask, primary ) )
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dk.cachet.carp.deployments.infrastructure

import dk.cachet.carp.common.application.ApplicationData
import dk.cachet.carp.common.application.data.input.InputDataType
import dk.cachet.carp.common.application.triggers.TaskControl
import dk.cachet.carp.common.application.users.ExpectedParticipantData
Expand Down Expand Up @@ -46,7 +47,7 @@ class PrimaryDeviceDeploymentTest
mapOf( 0 to trigger ),
setOf( TaskControl( 0, task.name, connected.roleName, TaskControl.Control.Start ) ),
setOf( expectedData ),
"some data"
ApplicationData( "some data" )
)

val json = JSON.encodeToString( deployment )
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dk.cachet.carp.deployments.infrastructure

import dk.cachet.carp.common.application.ApplicationData
import dk.cachet.carp.common.infrastructure.serialization.JSON
import dk.cachet.carp.deployments.application.users.StudyInvitation
import kotlinx.serialization.*
Expand All @@ -14,7 +15,7 @@ class StudyInvitationTest
@Test
fun can_serialize_and_deserialize_study_invitation_using_JSON()
{
val applicationData = """{"extraData":"42"}"""
val applicationData = ApplicationData( """{"extraData":"42"}""" )
val invitation = StudyInvitation( "Test", "Description", applicationData )

val serialized = JSON.encodeToString( invitation )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

package dk.cachet.carp.protocols.application

import dk.cachet.carp.common.application.ApplicationData
import dk.cachet.carp.common.application.UUID
import dk.cachet.carp.common.application.devices.AnyDeviceConfiguration
import dk.cachet.carp.common.application.devices.AnyPrimaryDeviceConfiguration
Expand All @@ -13,10 +14,9 @@ import dk.cachet.carp.common.application.users.AssignedTo
import dk.cachet.carp.common.application.users.ExpectedParticipantData
import dk.cachet.carp.common.application.users.ParticipantRole
import dk.cachet.carp.common.domain.Snapshot
import dk.cachet.carp.common.infrastructure.serialization.ApplicationDataSerializer
import dk.cachet.carp.protocols.domain.StudyProtocol
import kotlinx.datetime.Instant
import kotlinx.serialization.*
import kotlinx.serialization.Serializable
import kotlin.js.JsExport


Expand Down Expand Up @@ -45,8 +45,7 @@ data class StudyProtocolSnapshot(
*/
val assignedDevices: Map<String, Set<String>> = emptyMap(),
val expectedParticipantData: Set<ExpectedParticipantData> = emptySet(),
@Serializable( ApplicationDataSerializer::class )
val applicationData: String? = null
val applicationData: ApplicationData? = null
) : Snapshot<StudyProtocol>
{
@Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dk.cachet.carp.protocols.domain

import dk.cachet.carp.common.application.ApplicationData
import dk.cachet.carp.common.application.UUID
import dk.cachet.carp.common.application.devices.AnyDeviceConfiguration
import dk.cachet.carp.common.application.devices.AnyPrimaryDeviceConfiguration
Expand Down Expand Up @@ -482,7 +483,7 @@ class StudyProtocol(
* This can be used by infrastructures or concrete applications which require exchanging additional data
* between the protocols and clients subsystems, outside of scope or not yet supported by CARP core.
*/
var applicationData: String? = null
var applicationData: ApplicationData? = null


/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dk.cachet.carp.protocols.application

import dk.cachet.carp.common.application.ApplicationData
import dk.cachet.carp.common.application.UUID
import dk.cachet.carp.common.application.data.input.InputDataType
import dk.cachet.carp.common.application.devices.AnyDeviceConfiguration
Expand Down Expand Up @@ -130,7 +131,7 @@ class StudyProtocolSnapshotTest
description,
primaryDevices.toSet(), connectedDevices.toSet(), connections.toSet(),
tasks.toSet(), triggers, triggeredTasks.toSet(),
participantRoles.toSet(), assignedDevices, expectedParticipantData.toSet(), ""
participantRoles.toSet(), assignedDevices, expectedParticipantData.toSet(), ApplicationData( "" )
)
val reorganizedSnapshot = StudyProtocolSnapshot(
protocolId,
Expand All @@ -141,7 +142,7 @@ class StudyProtocolSnapshotTest
description,
primaryDevices.reversed().toSet(), connectedDevices.reversed().toSet(), connections.reversed().toSet(),
tasks.reversed().toSet(), triggers, triggeredTasks.reversed().toSet(),
participantRoles.reversed().toSet(), assignedDevices, expectedParticipantData.reversed().toSet(), ""
participantRoles.reversed().toSet(), assignedDevices, expectedParticipantData.reversed().toSet(), ApplicationData( "" )
)

assertEquals( snapshot, reorganizedSnapshot )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ private val phoneProtocol = StudyProtocol(
addConnectedDevice( bikeBeacon, phone )
addTaskControl( startOfStudyTrigger.start( measurePhoneMovement, phone ) )
addTaskControl( startOfStudyTrigger.start( measureBikeProximity, bikeBeacon ) )
applicationData = "{\"uiTheme\": \"black\"}"
applicationData = ApplicationData( "{\"uiTheme\": \"black\"}" )
}.getSnapshot()
private val startOfStudyTriggerId = phoneProtocol.triggers.entries.first { it.value == startOfStudyTrigger }.key
private val expectedParticipantData = setOf(
Expand Down Expand Up @@ -163,7 +163,7 @@ private val participantAccountId = UUID( "ca60cb7f-de18-44b6-baf9-3c8e6a73005a"
private val studyInvitation = StudyInvitation(
studyName,
"Participate in this study, which keeps track of how much you walk and bike!",
"{\"trialGroup\", \"A\"}"
ApplicationData ("{\"trialGroup\", \"A\"}" )
)
private val participantAssignedRoles = AssignedTo.Roles( setOf( participantRole.role ) )
private val participantInvitation = ParticipantInvitation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ declare module "@cachet/Kotlin-DateTime-library-kotlinx-datetime"
interface System
{
// now
q13(): Instant_0
p13(): Instant_0
}
function System_getInstance(): System

interface Instant_0
{
// toEpochMilliseconds
d14(): number
c14(): number
}
}
}
Loading

0 comments on commit 28d5e3a

Please sign in to comment.