-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
408 additions
and
48 deletions.
There are no files selected for viewing
101 changes: 101 additions & 0 deletions
101
carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/URL.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package dk.cachet.carp.common.application | ||
|
||
import dk.cachet.carp.common.infrastructure.serialization.createCarpStringPrimitiveSerializer | ||
import kotlinx.serialization.KSerializer | ||
import kotlinx.serialization.Serializable | ||
import kotlin.js.JsExport | ||
|
||
/** | ||
* Represents a Uniform Resource Locator (URL) according to RFC 3986. We use [URIRegex] to match | ||
* [stringRepresentation], but it cannot do actual validation. Therefore, [DefaultURLValidator] is used to validate | ||
* the URL. | ||
* | ||
* The URL of the web page which contains the task to be performed. | ||
* The URL may contain [URL.Variable] patterns which will be replaced with the corresponding values by the client | ||
* runtime. | ||
* | ||
* @param stringRepresentation The string representation of the URL. | ||
*/ | ||
@Serializable( URLSerializer::class ) | ||
@JsExport | ||
class URL ( val stringRepresentation: String ) | ||
{ | ||
init | ||
{ | ||
require( | ||
URIRegex.matches( stringRepresentation ) && DefaultURLValidator.isValid( stringRepresentation ) | ||
) | ||
{ | ||
"Invalid URI string representation." | ||
} | ||
} | ||
|
||
companion object | ||
{ | ||
private fun markup( name: String ) = name.uppercase().replace( ' ', '-' ).let { "{{$it}}" } | ||
} | ||
|
||
fun replaceVariables( participantId: UUID, deploymentId: UUID, triggerId: Int ): String = | ||
stringRepresentation.replace( Variable.PARTICIPANT_ID.pattern, participantId.toString() ) | ||
.replace( Variable.DEPLOYMENT_ID.pattern, deploymentId.toString() ) | ||
.replace( Variable.TRIGGER_ID.pattern, triggerId.toString() ) | ||
|
||
override fun equals( other: Any? ): Boolean | ||
{ | ||
if ( this === other ) return true | ||
if ( other !is URL ) return false | ||
|
||
return stringRepresentation == other.stringRepresentation | ||
} | ||
|
||
override fun hashCode(): Int = stringRepresentation.hashCode() | ||
|
||
override fun toString(): String = stringRepresentation | ||
|
||
enum class Variable( val pattern: String ) | ||
{ | ||
/** | ||
* Uniquely identifies the participant in the study. | ||
*/ | ||
PARTICIPANT_ID(markup("participant id")), | ||
|
||
/** | ||
* Uniquely identifies the deployment (group of participants and devices) of the study. | ||
*/ | ||
DEPLOYMENT_ID(markup("deployment id")), | ||
|
||
/** | ||
* Identifies the condition, defined by the study protocol, which causes the event to be triggered. | ||
*/ | ||
TRIGGER_ID(markup("trigger id")) | ||
} | ||
} | ||
|
||
/** | ||
* Regular expression to match any URI according to | ||
* [Appendix B of RFC 3986/STD 0066](https://www.rfc-editor.org/rfc/rfc3986#appendix-B) | ||
* | ||
* TODO: [URIRegex] allows us to write our own validation, but for the most part, what we have now is sufficient. | ||
*/ | ||
val URIRegex = Regex("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?") | ||
|
||
/** | ||
* A custom serializer for [URL]. | ||
*/ | ||
object URLSerializer : KSerializer<URL> by createCarpStringPrimitiveSerializer( { URL( it ) } ) | ||
|
||
/** | ||
* A default [URLValidator], which propagates the validation to the platform-specific implementation. | ||
* | ||
* NOTE: we do not do scheme-specific validation (e.g., checking if the host portion is not empty for http/https URLs). | ||
* This is because the URL class is used to represent any kind of URL, including custom schemes. | ||
*/ | ||
expect object DefaultURLValidator : URLValidator | ||
{ | ||
override fun isValid( url: String ): Boolean | ||
} | ||
|
||
interface URLValidator | ||
{ | ||
fun isValid( url: String ): Boolean | ||
} |
68 changes: 68 additions & 0 deletions
68
carp.common/src/commonMain/kotlin/dk/cachet/carp/common/application/devices/WebBrowser.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
@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.URL | ||
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.application.toTrilean | ||
import dk.cachet.carp.common.infrastructure.serialization.NotSerializable | ||
import kotlinx.serialization.Required | ||
import kotlinx.serialization.Serializable | ||
import kotlin.js.JsExport | ||
import kotlin.reflect.KClass | ||
|
||
|
||
@Serializable | ||
@JsExport | ||
data class WebBrowser( | ||
val possibleTypes: Set<String>, | ||
override val roleName: String, | ||
override val isOptional: Boolean = false | ||
) : PrimaryDeviceConfiguration<WebBrowserDeviceRegistration, WebBrowserDeviceRegistrationBuilder>() | ||
{ | ||
object Sensors : DataTypeSamplingSchemeMap() | ||
object Tasks : TaskConfigurationList() | ||
|
||
override fun getSupportedDataTypes(): Set<DataType> = Sensors.keys | ||
|
||
override val defaultSamplingConfiguration: Map<DataType, SamplingConfiguration> = emptyMap() | ||
|
||
override fun getDataTypeSamplingSchemes(): DataTypeSamplingSchemeMap = Sensors | ||
|
||
override fun createDeviceRegistrationBuilder(): WebBrowserDeviceRegistrationBuilder = | ||
WebBrowserDeviceRegistrationBuilder() | ||
|
||
override fun getRegistrationClass(): KClass<WebBrowserDeviceRegistration> = WebBrowserDeviceRegistration::class | ||
|
||
override fun isValidRegistration( registration: WebBrowserDeviceRegistration ): Trilean = | ||
possibleTypes.any { it.equals( registration.browser, ignoreCase = true ) }.toTrilean() | ||
} | ||
|
||
|
||
@Serializable | ||
@JsExport | ||
data class WebBrowserDeviceRegistration( | ||
val browser: String, | ||
val url: URL, | ||
@Required | ||
override val deviceDisplayName: String? = null | ||
) : DeviceRegistration() | ||
{ | ||
@Required | ||
override val deviceId: String = url.hashCode().toString() | ||
} | ||
|
||
|
||
@Suppress( "SERIALIZER_TYPE_INCOMPATIBLE" ) | ||
@Serializable( NotSerializable::class ) | ||
@JsExport | ||
class WebBrowserDeviceRegistrationBuilder : DeviceRegistrationBuilder<WebBrowserDeviceRegistration>() | ||
{ | ||
var browser: String = "" | ||
var url: String = "" | ||
override fun build(): WebBrowserDeviceRegistration = WebBrowserDeviceRegistration( browser, URL( url ) ) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
carp.common/src/commonTest/kotlin/dk/cachet/carp/common/application/URLTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package dk.cachet.carp.common.application | ||
|
||
import dk.cachet.carp.common.application.tasks.WebTask | ||
import dk.cachet.carp.common.application.tasks.WebTaskBuilder | ||
import dk.cachet.carp.common.infrastructure.serialization.createDefaultJSON | ||
import kotlin.test.Test | ||
import kotlin.test.assertEquals | ||
import kotlin.test.assertFailsWith | ||
|
||
class URLTest | ||
{ | ||
|
||
@Test | ||
fun can_serialize_and_deserialize_URL_using_JSON() | ||
{ | ||
val url = URL( "https://www.example.com" ) | ||
|
||
val json = createDefaultJSON() | ||
val serialized = json.encodeToString( URL.serializer(), url ) | ||
val parsed = json.decodeFromString( URL.serializer(), serialized ) | ||
|
||
assertEquals( url, parsed ) | ||
} | ||
|
||
@Test | ||
fun cant_initialize_url() | ||
{ | ||
// if string is empty | ||
assertFailsWith<IllegalArgumentException> { URL( "" ) } | ||
// with no scheme | ||
assertFailsWith<IllegalArgumentException> { URL ( "www.example.com" ) } | ||
} | ||
|
||
@Test | ||
fun can_initialize_url() | ||
{ | ||
// with custom scheme | ||
val url1 = URL( "app://welcome" ) | ||
// with explicit port | ||
val url2 = URL( "http://localhost:3000" ) | ||
// with query parameters | ||
val url3 = URL( "https://www.example.com?param1=value1¶m2=value2" ) | ||
// with redirect uri | ||
val url4 = URL( "https://www.example.com/?url=https://www.example2.com" ) | ||
|
||
assertEquals( "app://welcome", url1.stringRepresentation ) | ||
assertEquals( "http://localhost:3000", url2.stringRepresentation ) | ||
assertEquals( "https://www.example.com?param1=value1¶m2=value2", url3.stringRepresentation ) | ||
assertEquals( "https://www.example.com/?url=https://www.example2.com", url4.stringRepresentation ) | ||
} | ||
|
||
@Test | ||
fun url_variable_pattern_is_expected_format() | ||
{ | ||
val id = URL.Variable.PARTICIPANT_ID | ||
|
||
assertEquals( "{{PARTICIPANT-ID}}", id.pattern ) | ||
} | ||
|
||
@Test | ||
fun replaceVariables_succeeds() | ||
{ | ||
val surveyUrl = "http://awesomesurvey.com/42" | ||
val urlWithVariables = | ||
"$surveyUrl?" + | ||
"participantId=${URL.Variable.PARTICIPANT_ID.pattern}" + | ||
"&deploymentId=${URL.Variable.DEPLOYMENT_ID.pattern}" + | ||
"&triggerId=${URL.Variable.TRIGGER_ID.pattern}" | ||
|
||
val url = URL( urlWithVariables ) | ||
|
||
val participantId = UUID.randomUUID() | ||
val deploymentId = UUID.randomUUID() | ||
val triggerId = 42 | ||
val constructedUrl = url.replaceVariables( participantId, deploymentId, triggerId ) | ||
|
||
assertEquals( | ||
"$surveyUrl?participantId=$participantId&deploymentId=$deploymentId&triggerId=$triggerId", | ||
constructedUrl | ||
) | ||
} | ||
} |
Oops, something went wrong.