From 84f125e87de5ba366d074a36a3a21b6c6a5e7750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Mikul=C3=A1=C5=A1ek?= Date: Mon, 26 Feb 2024 07:38:46 +0100 Subject: [PATCH] feat: improve LoxoneEndpoint constructors --- examples/java/build.gradle.kts | 2 +- .../loxone/example/LoxoneClientExample.java | 2 +- .../src/main/kotlin/LoxoneClientExample.kt | 2 +- .../kotlin/LoxoneWebsocketClientExample.kt | 3 +- src/commonMain/kotlin/LoxoneEndpoint.kt | 73 +++++++++++++++++ src/commonMain/kotlin/LoxoneProfile.kt | 7 -- src/commonTest/kotlin/LoxoneEndpointTest.kt | 78 +++++++++++++++++++ 7 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 src/commonMain/kotlin/LoxoneEndpoint.kt create mode 100644 src/commonTest/kotlin/LoxoneEndpointTest.kt diff --git a/examples/java/build.gradle.kts b/examples/java/build.gradle.kts index 79d5425..875009b 100644 --- a/examples/java/build.gradle.kts +++ b/examples/java/build.gradle.kts @@ -8,7 +8,7 @@ repositories { } dependencies { - implementation("cz.smarteon.loxone:loxone-client-kotlin:0.1.0-SNAPSHOT") + implementation("cz.smarteon.loxone:loxone-client-kotlin:0.1.1-SNAPSHOT") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.1") } diff --git a/examples/java/src/main/java/cz/smarteon/loxone/example/LoxoneClientExample.java b/examples/java/src/main/java/cz/smarteon/loxone/example/LoxoneClientExample.java index 482d59c..d5a78e0 100644 --- a/examples/java/src/main/java/cz/smarteon/loxone/example/LoxoneClientExample.java +++ b/examples/java/src/main/java/cz/smarteon/loxone/example/LoxoneClientExample.java @@ -13,7 +13,7 @@ public class LoxoneClientExample { public static void main(String[] args) { System.out.println("Test"); - final var endpoint = new LoxoneEndpoint(args[0], 443); + final var endpoint = LoxoneEndpoint.fromUrl(args[0]) final LoxoneClient loxoneClient = new HttpLoxoneClient( endpoint, new LoxoneTokenAuthenticator( diff --git a/examples/kotlin/src/main/kotlin/LoxoneClientExample.kt b/examples/kotlin/src/main/kotlin/LoxoneClientExample.kt index 2835372..8fd4044 100644 --- a/examples/kotlin/src/main/kotlin/LoxoneClientExample.kt +++ b/examples/kotlin/src/main/kotlin/LoxoneClientExample.kt @@ -11,7 +11,7 @@ import cz.smarteon.loxone.message.ApiInfo suspend fun main(args: Array) { - val endpoint = LoxoneEndpoint(args[0], useSsl = true) + val endpoint = LoxoneEndpoint.fromUrl(args[0]) val loxoneClient: LoxoneClient = HttpLoxoneClient( endpoint, LoxoneTokenAuthenticator( diff --git a/examples/kotlin/src/main/kotlin/LoxoneWebsocketClientExample.kt b/examples/kotlin/src/main/kotlin/LoxoneWebsocketClientExample.kt index 00f665b..e23f61e 100644 --- a/examples/kotlin/src/main/kotlin/LoxoneWebsocketClientExample.kt +++ b/examples/kotlin/src/main/kotlin/LoxoneWebsocketClientExample.kt @@ -6,13 +6,12 @@ import cz.smarteon.loxone.LoxoneEndpoint import cz.smarteon.loxone.LoxoneProfile import cz.smarteon.loxone.LoxoneTokenAuthenticator import cz.smarteon.loxone.callForMsg -import cz.smarteon.loxone.ktor.HttpLoxoneClient import cz.smarteon.loxone.ktor.WebsocketLoxoneClient import cz.smarteon.loxone.message.ApiInfo suspend fun main(args: Array) { - val endpoint = LoxoneEndpoint(args[0], useSsl = false) + val endpoint = LoxoneEndpoint.FromUrl(args[0]) val loxoneClient: LoxoneClient = WebsocketLoxoneClient( endpoint, LoxoneTokenAuthenticator( diff --git a/src/commonMain/kotlin/LoxoneEndpoint.kt b/src/commonMain/kotlin/LoxoneEndpoint.kt new file mode 100644 index 0000000..e9a317a --- /dev/null +++ b/src/commonMain/kotlin/LoxoneEndpoint.kt @@ -0,0 +1,73 @@ +package cz.smarteon.loxone + +import io.ktor.http.* +import kotlin.jvm.JvmOverloads +import kotlin.jvm.JvmStatic + +/** + * Loxone connection endpoint representation. + * + * @param host Loxone host name or IP address, without protocol prefix, port or path. + * @param port Loxone port, default is 443 (HTTPS). + * @param useSsl Whether to use SSL, default is true. + * @param path Loxone path, url encoded, default is empty string. + */ +data class LoxoneEndpoint @JvmOverloads constructor( + val host: String, + val port: Int = HTTPS_PORT, + val useSsl: Boolean = true, + val path: String = "" +) { + + init { + require(host.isNotBlank()) { "Host must not be blank" } + require(!host.contains(":")) { "Host must not contain port or protocol" } + } + companion object { + private const val HTTP_PORT = 80 + private const val HTTPS_PORT = 443 + + /** + * Creates Loxone endpoint by parsing URL. + * @param url Loxone URL. + */ + @JvmStatic + fun fromUrl(url: String): LoxoneEndpoint { + val parsed = URLBuilder().takeFrom(url) + val port = when { + parsed.port > 0 -> parsed.port + parsed.protocol.isSecure() -> HTTPS_PORT + else -> HTTP_PORT + } + println(parsed) + println(parsed.encodedPath) + return LoxoneEndpoint(parsed.host, port, parsed.protocol.isSecure(), parsed.encodedPath) + } + + /** + * Creates Loxone endpoint for local connection. + * + * @param address Loxone IP address. + * @param port Loxone port, default is 80 (HTTP). + * @param path Loxone path, url encoded, default is empty string. + */ + @JvmStatic @JvmOverloads + fun local(address: String, port: Int = HTTP_PORT, path: String = ""): LoxoneEndpoint { + require(hostIsIp(address)) { "Local address must be IP" } + return LoxoneEndpoint(address, port, false, path) + } + + /** + * Creates Loxone endpoint for public domain connection. + * + * @param domain Loxone host domain. + * @param port Loxone port, default is 443 (HTTPS). + * @param path Loxone path, url encoded, default is empty string. + */ + @JvmStatic @JvmOverloads + fun public(domain: String, port: Int = HTTPS_PORT, path: String = ""): LoxoneEndpoint { + require(!hostIsIp(domain)) { "Public domain must not be IP" } + return LoxoneEndpoint(domain, port, true, path) + } + } +} diff --git a/src/commonMain/kotlin/LoxoneProfile.kt b/src/commonMain/kotlin/LoxoneProfile.kt index 81c3e52..df06d4f 100644 --- a/src/commonMain/kotlin/LoxoneProfile.kt +++ b/src/commonMain/kotlin/LoxoneProfile.kt @@ -2,13 +2,6 @@ package cz.smarteon.loxone import kotlin.jvm.JvmOverloads -data class LoxoneEndpoint @JvmOverloads constructor( - val host: String, - val port: Int = 80, - val useSsl: Boolean = true, - val path: String = "" -) - data class LoxoneCredentials @JvmOverloads constructor( val username: String, val password: String, diff --git a/src/commonTest/kotlin/LoxoneEndpointTest.kt b/src/commonTest/kotlin/LoxoneEndpointTest.kt new file mode 100644 index 0000000..8168f35 --- /dev/null +++ b/src/commonTest/kotlin/LoxoneEndpointTest.kt @@ -0,0 +1,78 @@ +package cz.smarteon.loxone + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.WordSpec +import io.kotest.datatest.withData +import io.kotest.matchers.shouldBe + +class LoxoneEndpointTest : WordSpec({ + + "Loxone endpoint constructed" When { + + "with default values" should { + LoxoneEndpoint("somehost").apply { + "have host" { host shouldBe "somehost"} + "have port" { port shouldBe 443 } + "have useSsl" { useSsl shouldBe true } + "have path" { path shouldBe "" } + } + } + + withData( + nameFn = { (case, _) -> "with $case host" }, + "empty" to "", + "contains port" to "host:123", + "contains protocol" to "http://host" + ) { (_, host) -> + shouldThrow { + LoxoneEndpoint(host) + } + } + } + + "FromUrl endpoint constructed" When { + withData( + nameFn = { (url, _) -> url }, + "http://some:7780/testPath" to LoxoneEndpoint("some", 7780, false, "/testPath"), + "http://some/testPath" to LoxoneEndpoint("some", 80, false, "/testPath"), + "https://some.smarteon.cz:7743/testPath" to LoxoneEndpoint("some.smarteon.cz", 7743, true, "/testPath"), + "https://some.smarteon.cz/testPath" to LoxoneEndpoint("some.smarteon.cz", 443, true, "/testPath") + ) { (url, expected) -> + LoxoneEndpoint.fromUrl(url) shouldBe expected + } + } + + "Local endpoint constructed" When { + "with default values" should { + LoxoneEndpoint.local("192.168.9.77").apply { + "have host" { host shouldBe "192.168.9.77" } + "have port" { port shouldBe 80 } + "have useSsl" { useSsl shouldBe false } + "have path" { path shouldBe "" } + } + } + + "with non-IP address" should { + shouldThrow { + LoxoneEndpoint.local("somehost") + } + } + } + + "Public domain endpoint constructed" When { + "with default values" should { + LoxoneEndpoint.public("test.smarteon.cz").apply { + "have host" { host shouldBe "test.smarteon.cz" } + "have port" { port shouldBe 443 } + "have useSsl" { useSsl shouldBe true } + "have path" { path shouldBe "" } + } + } + + "with non-IP address" should { + shouldThrow { + LoxoneEndpoint.public("192.168.9.77") + } + } + } +})