Skip to content

Commit

Permalink
Add ApplicationData to trigger associated serializer
Browse files Browse the repository at this point in the history
This way, the serializer doesn't need to be specified on every field which requires `ApplicationDataSerializer`. Specifically, in case of inheritance, this prevents having to apply the serializer to all inheriting classes on base properties which need it.
  • Loading branch information
yuanchen233 authored and Whathecode committed Oct 5, 2024
1 parent 7b6fa58 commit 94664ce
Show file tree
Hide file tree
Showing 17 changed files with 71 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dk.cachet.carp.common.application

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


/**
* Holds extra [data] which is specific to concrete applications, sensors, or infrastructure, and isn't statically
* known to the base infrastructure.
*
* While the [data] can be formatted in any way, when JSON serialization is applied and [data] contains a JSON element,
* the data will be formatted as JSON (without escaping special characters). If the JSON contained in the string is
* malformed, it will be serialized as a normal, escaped string.
*/
@Serializable( ApplicationDataSerializer::class )
@JsExport
data class ApplicationData( val data: String )
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,36 +9,36 @@ import kotlinx.serialization.json.*


/**
* Serializes [String] 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.
* Tries to serialize the data contained in [ApplicationData] as a [JsonElement],
* but only in case a JSON encoder/decoder is used.
* In case the JSON contained in the text 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
else originalString.substring( 1, originalString.length - 1 )
val data =
if ( jsonElement is JsonObject ) originalString
else originalString.substring( 1, originalString.length - 1 )

return ApplicationData( data )
}

override fun serialize( encoder: Encoder, value: String? )
override fun serialize( encoder: Encoder, value: ApplicationData? )
{
// Early out when the value is null.
if ( value == null )
Expand All @@ -49,17 +50,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 +69,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,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,7 +14,6 @@ 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.*
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 @@ -57,7 +58,7 @@ interface ParticipationServiceTest
val (participationService, deploymentService, accountService) = createSUT()
val protocol = createSinglePrimaryDeviceProtocol()
val identity = AccountIdentity.fromEmailAddress( "test@test.com" )
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,7 +14,6 @@ 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.*
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
}
}
}
4 changes: 2 additions & 2 deletions typescript-declarations/carp-kotlinx-datetime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ declare module "@cachet/Kotlin-DateTime-library-kotlinx-datetime"


// Implement base interfaces in internal types.
extend.$_$.System.prototype.now = function(): kotlinx.datetime.Instant { return this.q13(); };
extend.$_$.Instant_0.prototype.toEpochMilliseconds = function(): number { return this.d14(); };
extend.$_$.System.prototype.now = function(): kotlinx.datetime.Instant { return this.p13(); };
extend.$_$.Instant_0.prototype.toEpochMilliseconds = function(): number { return this.c14(); };


// Export facade.
Expand Down
Loading

0 comments on commit 94664ce

Please sign in to comment.