Skip to content

Commit

Permalink
Add ApplicationData datatype
Browse files Browse the repository at this point in the history
  • Loading branch information
yuanchen233 committed Oct 4, 2024
1 parent 4c78321 commit 9001f8c
Show file tree
Hide file tree
Showing 26 changed files with 98 additions and 61 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 @@ -2,6 +2,7 @@

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

import dk.cachet.carp.common.application.ApplicationData
import dk.cachet.carp.common.application.Trilean
import dk.cachet.carp.common.application.UUID
import dk.cachet.carp.common.application.data.CarpDataTypes
Expand Down Expand Up @@ -79,7 +80,7 @@ data class AltBeaconDeviceRegistration(
val referenceRssi: Short,
@Required
override val deviceDisplayName: String? = null, // TODO: We could map known manufacturerId's to display names.
override val additionalSpecifications: String? = null
override val additionalSpecifications: ApplicationData? = null
) : DeviceRegistration()
{
companion object
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dk.cachet.carp.common.application.devices

import dk.cachet.carp.common.application.ApplicationData
import dk.cachet.carp.common.infrastructure.serialization.NotSerializable
import kotlinx.serialization.*
import kotlin.js.JsExport
Expand All @@ -16,7 +17,7 @@ data class BLESerialNumberDeviceRegistration(
val serialNumber: String,
@Required
override val deviceDisplayName: String? = null,
override val additionalSpecifications: String? = null
override val additionalSpecifications: ApplicationData? = null
) : DeviceRegistration()
{
init
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dk.cachet.carp.common.application.devices

import dk.cachet.carp.common.application.ApplicationData
import dk.cachet.carp.common.application.UUID
import dk.cachet.carp.common.infrastructure.serialization.NotSerializable
import kotlinx.serialization.*
Expand All @@ -16,7 +17,7 @@ import kotlin.js.JsExport
data class DefaultDeviceRegistration(
@Required
override val deviceDisplayName: String? = null,
override val additionalSpecifications: String? = null,
override val additionalSpecifications: ApplicationData? = null,
@Required
override val deviceId: String = UUID.randomUUID().toString(),
) : DeviceRegistration()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

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

import dk.cachet.carp.common.application.ApplicationData
import dk.cachet.carp.common.application.Immutable
import dk.cachet.carp.common.application.ImplementAsDataClass
import dk.cachet.carp.common.infrastructure.serialization.ApplicationDataSerializer
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 Expand Up @@ -48,8 +50,7 @@ abstract class DeviceRegistration
* Additional specifications which may be relevant to the researcher when interpreting collected data.
* E.g., brand/model name, operating system version, or any other relevant information.
*/
@Serializable( ApplicationDataSerializer::class )
abstract val additionalSpecifications: String?
abstract val additionalSpecifications: ApplicationData?
}


Expand Down Expand Up @@ -77,7 +78,7 @@ abstract class DeviceRegistrationBuilder<T : DeviceRegistration>
* Additional specifications which may be relevant to the researcher when interpreting collected data.
* E.g., brand/model name, operating system version, or any other relevant information.
*/
var additionalSpecifications: String? = null
var additionalSpecifications: ApplicationData? = null

/**
* Build the immutable [DeviceRegistration] using the current configuration of this [DeviceRegistrationBuilder].
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dk.cachet.carp.common.application.devices

import dk.cachet.carp.common.application.ApplicationData
import dk.cachet.carp.common.application.MACAddress
import dk.cachet.carp.common.infrastructure.serialization.NotSerializable
import kotlinx.serialization.*
Expand All @@ -15,7 +16,7 @@ data class MACAddressDeviceRegistration(
val macAddress: MACAddress,
@Required
override val deviceDisplayName: String? = null,
override val additionalSpecifications: String? = null
override val additionalSpecifications: ApplicationData? = null
) : DeviceRegistration()
{
@Required
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

import dk.cachet.carp.common.application.ApplicationData
import dk.cachet.carp.common.application.Trilean
import dk.cachet.carp.common.application.data.DataType
import dk.cachet.carp.common.application.sampling.DataTypeSamplingSchemeMap
Expand Down Expand Up @@ -52,7 +53,7 @@ data class WebsiteDeviceRegistration(
val userAgent: String,
@Required
override val deviceDisplayName: String? = userAgent,
override val additionalSpecifications: String? = null
override val additionalSpecifications: ApplicationData? = null
) : DeviceRegistration()
{
@Required
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 dk.cachet.carp.common.application.Trilean
import dk.cachet.carp.common.application.data.DataType
import dk.cachet.carp.common.application.devices.AnyDeviceConfiguration
Expand Down Expand Up @@ -179,12 +180,12 @@ data class CustomDeviceRegistration internal constructor(
private data class BaseMembers(
override val deviceId: String,
override val deviceDisplayName: String?,
override val additionalSpecifications: String? = null
override val additionalSpecifications: ApplicationData? = null
) : DeviceRegistration()

override val deviceId: String
override val deviceDisplayName: String?
override val additionalSpecifications: String?
override val additionalSpecifications: ApplicationData?

init
{
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 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
@@ -1,5 +1,6 @@
package dk.cachet.carp.common.infrastructure.serialization

import dk.cachet.carp.common.application.ApplicationData
import dk.cachet.carp.common.application.devices.DefaultDeviceRegistration
import dk.cachet.carp.common.application.devices.DeviceRegistration
import dk.cachet.carp.common.infrastructure.test.makeUnknown
Expand Down Expand Up @@ -59,7 +60,7 @@ class DeviceRegistrationTest
// With all nullable fields set.
DefaultDeviceRegistration(
deviceDisplayName = "Device name",
additionalSpecifications = """{"OS":"Android 42"}"""
additionalSpecifications = ApplicationData( """{"OS":"Android 42"}""" )
)
)

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,9 +14,8 @@ 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.Instant
import kotlinx.serialization.*
import kotlinx.serialization.Serializable
import kotlin.js.JsExport


Expand Down Expand Up @@ -63,8 +63,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
@@ -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.devices.DefaultDeviceRegistration
import dk.cachet.carp.common.application.devices.DeviceRegistration
Expand Down Expand Up @@ -158,7 +159,7 @@ class ValidationTest
{
override val deviceId: String = "Invalid"
override val deviceDisplayName: String? = null
override val additionalSpecifications: String? = null
override val additionalSpecifications: ApplicationData? = null
}
val preregistrations = mapOf( connectedRoleName to invalidRegistration )

Expand Down
Loading

0 comments on commit 9001f8c

Please sign in to comment.