Skip to content

Commit 4653294

Browse files
authored
Merge pull request hexagontk#447 from hexagonkt/develop
Add converters support
2 parents dfcd2c5 + 52e55e3 commit 4653294

File tree

9 files changed

+193
-75
lines changed

9 files changed

+193
-75
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.hexagonkt.core.converters
2+
3+
import kotlin.reflect.KClass
4+
5+
/**
6+
* Utility method to convert one type to another.
7+
*
8+
* @param target Target type for the source instance.
9+
* @receiver Value to convert to another type.
10+
*
11+
* @see ConvertersManager.convert
12+
*/
13+
fun <T : Any> Any.convert(target: KClass<T>): T =
14+
ConvertersManager.convert(this, target)
15+
16+
/**
17+
* Utility method to convert one type to another.
18+
*
19+
* @param T Target type for the source instance.
20+
* @receiver Value to convert to another type.
21+
*
22+
* @see ConvertersManager.convert
23+
*/
24+
inline fun <reified T : Any> Any.convert(): T =
25+
convert(T::class)
26+
27+
// TODO Add conversion utilities to transform collections (lists, sets or maps)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.hexagonkt.core.converters
2+
3+
import kotlin.reflect.KClass
4+
5+
/**
6+
* Registry that holds functions to convert from one type to another.
7+
*
8+
* @sample com.hexagonkt.core.converters.ConvertersManagerTest.usageExample
9+
*/
10+
object ConvertersManager {
11+
12+
private var converters: Map<Pair<*, *>, (Any) -> Any> = emptyMap()
13+
14+
/**
15+
* Register a mapping function from one type to another.
16+
*
17+
* @param key Pair which key is the source type and the value is the target type.
18+
* @param block Block that converts an instance of the source type to the target one.
19+
*/
20+
@Suppress("UNCHECKED_CAST") // Type consistency is checked at runtime
21+
fun <S : Any, T : Any> register(key: Pair<KClass<S>, KClass<T>>, block: (S) -> T) {
22+
converters = converters + (key as Pair<*, *> to block as (Any) -> Any)
23+
}
24+
25+
/**
26+
* Delete an existing mapping by its key.
27+
*
28+
* @param key Key of the mapping to be removed. No error is triggered if key doesn't exist.
29+
*/
30+
fun remove(key: Pair<KClass<*>, KClass<*>>) {
31+
converters = converters - key
32+
}
33+
34+
/**
35+
* Convert one type to another using the registered mapper function among both types. If no
36+
* mapper function is defined for the specified types, an exception is thrown.
37+
*
38+
* @param source Value to convert to another type.
39+
* @param target Target type for the source instance.
40+
*/
41+
@Suppress("UNCHECKED_CAST") // Type consistency is checked at runtime
42+
fun <S : Any, T : Any> convert(source: S, target: KClass<T>): T =
43+
converters[source::class to target]
44+
?.invoke(source) as? T
45+
?: error("No converter for ${source::class.simpleName} -> ${target.simpleName}")
46+
}

core/src/main/kotlin/helpers/Strings.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ fun String.decodeBase64(): ByteArray =
5151
*
5252
* @param parameters The map with the list of key/value tuples.
5353
* @return The filtered text or the same string if no values are passed or found in the text.
54-
* @sample com.hexagonkt.helpers.StringsSamplesTest.filterVarsExample
54+
* @sample com.hexagonkt.core.helpers.StringsSamplesTest.filterVarsExample
5555
*/
5656
fun String.filterVars(parameters: Map<*, *>): String =
5757
parameters.entries
@@ -71,7 +71,7 @@ fun String.filterVars(parameters: Map<*, *>): String =
7171
*
7272
* @param parameters vararg of key/value pairs.
7373
* @return The filtered text or the same string if no values are passed or found in the text.
74-
* @sample com.hexagonkt.helpers.StringsSamplesTest.filterVarsVarargExample
74+
* @sample com.hexagonkt.core.helpers.StringsSamplesTest.filterVarsVarargExample
7575
*
7676
* @see filterVars
7777
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package com.hexagonkt.core.converters
2+
3+
import com.hexagonkt.core.helpers.get
4+
import com.hexagonkt.core.helpers.fail
5+
import org.junit.jupiter.api.Test
6+
import java.lang.IllegalStateException
7+
import java.net.URL
8+
import java.nio.ByteBuffer
9+
import java.time.LocalDate
10+
import java.time.LocalDateTime
11+
import java.time.LocalTime
12+
import java.util.*
13+
import kotlin.test.assertEquals
14+
import kotlin.test.assertFailsWith
15+
16+
internal class ConvertersManagerTest {
17+
18+
internal data class Person(val givenName: String, val familyName: String)
19+
20+
internal data class Company(
21+
val id: String,
22+
val foundation: LocalDate,
23+
val closeTime: LocalTime,
24+
val openTime: ClosedRange<LocalTime>,
25+
val web: URL? = null,
26+
val clients: List<URL> = listOf(),
27+
val logo: ByteBuffer? = null,
28+
val notes: String? = null,
29+
val people: Set<Person> = emptySet(),
30+
val creationDate: LocalDateTime = LocalDateTime.now(),
31+
)
32+
33+
@Test fun `Type conversion works properly`() {
34+
ConvertersManager.register(Date::class to String::class) { it.toString() }
35+
val dateText1 = ConvertersManager.convert(Date(), String::class)
36+
val dateText2 = Date().convert(String::class)
37+
val dateText3 = Date().convert<String>()
38+
assertEquals(dateText1, dateText2)
39+
assertEquals(dateText2, dateText3)
40+
ConvertersManager.remove(Date::class to String::class)
41+
val e = assertFailsWith<IllegalStateException> { Date().convert<String>() }
42+
val source = Date::class.simpleName
43+
val target = String::class.simpleName
44+
assertEquals("No converter for $source -> $target", e.message)
45+
}
46+
47+
@Test fun `Nested type conversion works properly`() {
48+
ConvertersManager.register(Person::class to Map::class, ::personToMap)
49+
ConvertersManager.register(Company::class to Map::class, ::companyToMap)
50+
51+
val date = LocalDate.now()
52+
val time = LocalTime.now()
53+
val openTime = time.minusMinutes(1)..time.plusMinutes(1)
54+
55+
Company("1", date, time, openTime).let {
56+
val m: Map<String, *> = it.convert()
57+
assertEquals("1", m[Company::id.name])
58+
}
59+
60+
Company("1", date, time, openTime, people = setOf(Person("John", "Smith"))).let {
61+
val m: Map<String, *> = it.convert()
62+
val persons = m[Company::people.name, 0] as? Map<*, *> ?: fail
63+
assertEquals("John", persons[Person::givenName.name])
64+
assertEquals("Smith", persons[Person::familyName.name])
65+
}
66+
}
67+
68+
@Test fun `Delete non existing key don't generate errors`() {
69+
ConvertersManager.remove(Int::class to Date::class)
70+
}
71+
72+
@Test fun usageExample() {
73+
// Define a mapper from a source type to a target type
74+
ConvertersManager.register(Date::class to String::class) { it.toString() }
75+
76+
// Conversions can be done with different utility methods
77+
val directConversion = ConvertersManager.convert(Date(), String::class)
78+
val utilityConversion = Date().convert(String::class)
79+
val reifiedUtilityConversion = Date().convert<String>()
80+
81+
assertEquals(directConversion, utilityConversion)
82+
assertEquals(utilityConversion, reifiedUtilityConversion)
83+
84+
// Conversion mappers can be deleted
85+
ConvertersManager.remove(Date::class to String::class)
86+
87+
// Trying to perform a conversion that has not a registered mapper fails with an error
88+
val e = assertFailsWith<IllegalStateException> { Date().convert<String>() }
89+
val source = Date::class.simpleName
90+
val target = String::class.simpleName
91+
assertEquals("No converter for $source -> $target", e.message)
92+
}
93+
94+
private fun personToMap(person: Person): Map<String, *> =
95+
mapOf(
96+
"givenName" to person.givenName,
97+
"familyName" to person.familyName,
98+
)
99+
100+
private fun companyToMap(company: Company): Map<String, *> =
101+
mapOf(
102+
"id" to company.id,
103+
"foundation" to company.foundation,
104+
"closeTime" to company.closeTime,
105+
"openTime" to company.openTime,
106+
"web" to company.web,
107+
"clients" to company.clients,
108+
"logo" to company.logo,
109+
"notes" to company.notes,
110+
"people" to company.people.map { it.convert<Map<String, Any>>() }.toList(),
111+
"creationDate" to company.creationDate,
112+
)
113+
}

gradle.properties

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ org.gradle.console=plain
66
org.gradle.dependency.verification.console=verbose
77

88
# Gradle
9-
version=1.4.8
9+
version=1.4.9
1010
group=com.hexagonkt
1111
description=The atoms of your platform
1212

@@ -41,7 +41,7 @@ kotlinVersion=1.5.31
4141
dokkaVersion=1.5.31
4242
mockkVersion=1.12.0
4343
junitVersion=5.8.1
44-
mkdocsMaterialVersion=7.3.5
44+
mkdocsMaterialVersion=7.3.6
4545

4646
# http_server_servlet
4747
servletVersion=3.1.0

gradle/dokka.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ private void setUpDokka(final Object dokkaTask) {
3434
includeNonPublic.set(false)
3535
reportUndocumented.set(false)
3636
includes.from(fileTree(projectDir) { include("*.md") })
37-
samples.from(fileTree(projectDir) { include("**/*SamplesTest.kt") })
37+
samples.from(fileTree(projectDir) { include("**/*Test.kt") })
3838
}
3939
}
4040
}

site/assets/service_worker.js

-55
This file was deleted.

site/mkdocs/main.html

-9
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,6 @@
3939
<link rel="author" href="/humans.txt" />
4040
<link rel="manifest" href="/manifest.json">
4141

42-
<!-- Web worker -->
43-
<!--
44-
<script>
45-
if ('serviceWorker' in navigator)
46-
navigator.serviceWorker
47-
.register('/service_worker.js')
48-
.then(function() { console.log('Worker Registered'); });
49-
</script>
50-
-->
5142
{% endblock %}
5243

5344
{% block footer %}

site/pages/developer_guide.md

+2-6
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ The project is composed of modules, each module provides a single functionality.
2626
kinds of modules:
2727

2828
* The ones that provide functionality that does not depend on different implementations, like
29-
[hexagon_scheduler] or [core]. Their name always starts with the `hexagon_` prefix. These
30-
modules can depend on several Ports, but never on Adapters (see below).
29+
[core]. Their name always starts with the `hexagon_` prefix. These modules can depend on several
30+
Ports, but never on Adapters (see below).
3131
* Modules that define one or more related "Ports": these are interfaces to a feature that may have
3232
different implementations (i.e., [http_server] or [templates]). They cannot be used by
3333
themselves and in their place, an adapter implementing them should be added to the list of
@@ -37,7 +37,6 @@ kinds of modules:
3737
[http_server_jetty] are examples of this type of module. Adapter names must start with their
3838
port name.
3939

40-
[hexagon_scheduler]: /hexagon_scheduler/
4140
[core]: /core/
4241

4342
[http_server]: /http_server/
@@ -67,12 +66,9 @@ The main features are the following:
6766
The following libraries provide extra features not bound to different implementations. They will not
6867
use dependencies outside the Hexagon toolkit.
6968

70-
* [Scheduling]: this module allows services to execute tasks periodically using Cron expressions.
71-
However, you have to be careful to not run tasks twice if you have many instances.
7269
* [Web]: this module is meant to ease web application development. Provides helpers for
7370
generating HTML and depends on the [HTTP Server] and [Templates] ports.
7471

75-
[Scheduling]: /hexagon_scheduler/
7672
[Web]: /web/
7773

7874
# Toolkit Ports

0 commit comments

Comments
 (0)