Skip to content

Commit 39386b3

Browse files
committed
Merge branch 'refactor' (yippieee)
2 parents f247d4e + 990f920 commit 39386b3

47 files changed

Lines changed: 980 additions & 403 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ websockets
1313
- [ ] current status
1414
- [ ] broadcast details on mqtt
1515
- [ ] playlist generation based on stats ("kelder toppers" etc)
16+
17+
## do next
18+
19+
- fix http error handling in `Http.kt`

build.gradle

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,28 @@ dependencies {
2626
implementation 'org.springframework.boot:spring-boot-starter-actuator'
2727
// implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'// management stuff
2828
// runtimeOnly 'org.postgresql:postgresql'
29+
2930
testImplementation 'org.springframework.boot:spring-boot-starter-test'
3031
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
3132

33+
3234
// -- kotlin --
3335
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
3436
implementation "com.fasterxml.jackson.module:jackson-module-kotlin"
3537
implementation "org.jetbrains.kotlin:kotlin-reflect"
38+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"
3639

3740
// -- mqtt --
3841
implementation "org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5"
42+
implementation "com.hivemq:hivemq-mqtt-client:1.3.12"
3943

44+
// -- misc --
45+
implementation 'com.github.f4b6a3:uuid-creator:6.1.0'
4046
}
4147

42-
tasks.named('test') {
43-
useJUnitPlatform()
44-
}
48+
//tasks.named('test') {
49+
// useJUnitPlatform()
50+
//}
4551

4652
// https://kotlinlang.org/docs/annotations.html#all-meta-target
4753
kotlin {

example.env

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
SPOTIFY_CLIENT_ID=ec3a2aa161a6410a8961f1eaf4b3f2be
2+
SPOTIFY_CLIENT_SECRET=e34e28e4d41145a5bd934cfa511e902c
3+
MQTT_SERVER_URL=koin
4+
MQTT_SERVER_PORT=1883
5+
MQTT_LIBRESPOT_LISTEN_TOPIC=music/events/playing
6+
MQTT_ZODOM_LISTEN_TOPIC=music/votes
7+
MQTT_PUBLISH_TOPIC=music/current_song_info_localtest
8+
ZODOM_API_URL=http://localhost:3000
Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,24 @@
11
package gent.zeus.guitar
22

3-
import gent.zeus.guitar.mqtt.MqttEnv
4-
import gent.zeus.guitar.spotify.SpotifyToken
5-
import org.springframework.boot.CommandLineRunner
3+
import gent.zeus.guitar.mqtt.MqttContext
4+
import kotlinx.coroutines.coroutineScope
5+
import kotlinx.coroutines.delay
6+
import kotlinx.coroutines.launch
67
import org.springframework.boot.autoconfigure.SpringBootApplication
78
import org.springframework.boot.runApplication
8-
import org.springframework.core.Ordered
9-
import org.springframework.core.annotation.Order
10-
import org.springframework.stereotype.Component
11-
import kotlin.system.exitProcess
9+
import kotlin.time.Duration.Companion.seconds
1210

1311

1412
@SpringBootApplication
1513
open class Application
1614

17-
fun main(args: Array<String>) {
18-
runApplication<Application>(*args)
19-
}
20-
21-
@Component
22-
@Order(Ordered.HIGHEST_PRECEDENCE)
23-
class DoStartupChecks : CommandLineRunner {
24-
25-
private val checklist: List<StartupCheck> = listOf(
26-
SpotifyToken,
27-
MqttEnv,
28-
)
15+
suspend fun main(args: Array<String>): Unit = coroutineScope {
16+
Environment.SPOTIFY_CLIENT_ID // access variable to make singleton object
2917

30-
override fun run(vararg args: String?) {
31-
Logging.log.info("running startup checks...")
32-
33-
checklist.map { it.checkOnStartup() }.map {
34-
if (!it.checkPassed) {
35-
Logging.log.error(it.message)
36-
}
37-
it
38-
}.let { checklist ->
39-
if (checklist.any { !it.checkPassed }) {
40-
exitProcess(1)
41-
}
42-
}
43-
44-
Logging.log.info("startup checks complete!")
18+
launch {
19+
delay(1.seconds)
20+
MqttContext().startMqtt()
4521
}
22+
23+
runApplication<Application>(*args)
4624
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package gent.zeus.guitar
2+
3+
import kotlin.system.exitProcess
4+
5+
/**
6+
* singleton object for environment variables
7+
*/
8+
object Environment {
9+
private var loadFailed = false
10+
11+
val SPOTIFY_CLIENT_ID = loadRequired("SPOTIFY_CLIENT_ID")
12+
val SPOTIFY_CLIENT_SECRET = loadRequired("SPOTIFY_CLIENT_SECRET")
13+
14+
val MQTT_HOST = load("MQTT_HOST".also { notifyDisabled("mqtt", it) })
15+
val MQTT_PORT = load("MQTT_PORT")
16+
val MQTT_LIBRESPOT_LISTEN_TOPIC = load("MQTT_LIBRESPOT_LISTEN_TOPIC")
17+
val MQTT_ZODOM_LISTEN_TOPIC = load("MQTT_ZODOM_LISTEN_TOPIC")
18+
val MQTT_PUBLISH_TOPIC = load("MQTT_PUBLISH_TOPIC")
19+
20+
val ZODOM_API_URL = load("ZODOM_API_URL".also { notifyDisabled("zodom data", it) })
21+
22+
init {
23+
if (loadFailed) exitProcess(1)
24+
}
25+
26+
/**
27+
* load required environment variable
28+
*
29+
* if it is empty/not set, print an error and (indirectly) end the application
30+
*
31+
* @return the value of the variable
32+
*/
33+
private fun loadRequired(key: String): String = System.getenv(key).takeUnless { it?.isEmpty() ?: true } ?: run {
34+
logger.error("environment variable $key required but not set!")
35+
loadFailed = true
36+
return ""
37+
}
38+
39+
/**
40+
* load a non-required environment variable
41+
*
42+
* @return the value of the variable, or an empty string if it is not set
43+
*/
44+
private fun load(key: String): String = System.getenv(key).takeUnless { it?.isEmpty() ?: true } ?: ""
45+
46+
private fun notifyDisabled(what: String, envVariableKey: String) {
47+
if (!load(envVariableKey).isEmpty()) return
48+
logger.warn("$what is disabled because environment variable $envVariableKey is not set")
49+
}
50+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package gent.zeus.guitar
2+
3+
/**
4+
* error generated by the application that can be returned to the user
5+
*
6+
* @param message message of the error
7+
* @param remoteError error from the remote that caused this error
8+
*/
9+
abstract class DataFetchError(val message: String, val remoteError: String?, val httpStatusCode: Int) {
10+
11+
override fun toString(): String {
12+
return this::class.simpleName ?: "anonymous class"
13+
}
14+
}
15+
16+
/**
17+
* error caused by user wrongdoing
18+
*/
19+
open class UserError(message: String, remoteError: String?, httpStatusCode: Int = 400) :
20+
DataFetchError(message, remoteError, httpStatusCode)
21+
22+
/**
23+
* error caused by server mishaps
24+
*/
25+
open class ServerError(message: String, remoteError: String?, httpStatusCode: Int = 500) :
26+
DataFetchError(message, remoteError, httpStatusCode)
27+
28+
class MultiError(errors: List<DataFetchError>) : DataFetchError(
29+
message = errors.joinToString(
30+
", ",
31+
if (errors.size > 1) "multiple errors were encountered: " else "",
32+
) { "${it.message} (${it.httpStatusCode})" },
33+
remoteError = errors.joinToString(
34+
"; ",
35+
) { "${it.remoteError}" },
36+
httpStatusCode = when {
37+
errors.any { it is UserError && it.httpStatusCode == 404 } -> 404
38+
errors.any { it is UserError } -> 400
39+
else -> 500
40+
}
41+
)
42+
43+
/**
44+
* success or error when handling musical data
45+
*/
46+
sealed class DataResult<out T> {
47+
data class Ok<T>(val value: T) : DataResult<T>()
48+
data class Error<E : DataFetchError>(val error: E) : DataResult<Nothing>()
49+
}
50+
51+
/**
52+
* log all the errors in the list
53+
* @param pre: bit to put before the error message
54+
*/
55+
fun <E : DataFetchError> Iterable<E>.logErrors(pre: String) = onEach {
56+
logger.error("$pre ${it.message} (${it.httpStatusCode})")
57+
}
58+
59+
/**
60+
* tries to execute the code block. if the code throws an exception, it makes an error log statement
61+
* and re-throws the exception
62+
*
63+
* @param messageOnFail message to put in log statement (will append ` (stacktrace below)`)
64+
* @param block code block to try executing
65+
*/
66+
inline fun logExceptionFail(messageOnFail: String, block: () -> Unit) {
67+
try {
68+
block()
69+
} catch (e: Exception) {
70+
logger.error("$messageOnFail: ${e.message} (stacktrace below)")
71+
throw e
72+
}
73+
}
74+
75+
inline fun <R> logExceptionWarn(messageOnFail: String, block: () -> R) =
76+
try {
77+
block()
78+
} catch (e: Exception) {
79+
logger.warn("$messageOnFail: ${e.message} (stacktrace below)")
80+
e.printStackTrace()
81+
null
82+
}
83+
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
package gent.zeus.guitar
22

3-
import org.springframework.web.client.RestClient
43

5-
val REST_CLIENT: RestClient = RestClient.create()
6-
7-
const val SPOTIFY_API_URL = "https://api.spotify.com/v1"
8-
const val ZODOM_API_URL = "http://localhost:3000"
4+
const val SPOTIFY_API_URL = "https://api.spotify.com/v1"
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package gent.zeus.guitar
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper
4+
import org.springframework.http.HttpHeaders
5+
import org.springframework.http.MediaType
6+
import org.springframework.http.ResponseEntity
7+
import org.springframework.web.client.HttpClientErrorException
8+
import org.springframework.web.client.RestClient
9+
import org.springframework.web.client.body
10+
import org.springframework.web.client.toEntity
11+
12+
val REST_CLIENT: RestClient = RestClient.create()
13+
14+
inline fun <reified T : Any> httpRequestIntoObj(uri: String, bearerToken: String? = null): HttpResponse<T> =
15+
try {
16+
REST_CLIENT.get()
17+
.uri(uri)
18+
.let {
19+
bearerToken ?: return@let it
20+
it.header(HttpHeaders.AUTHORIZATION, "Bearer $bearerToken")
21+
}
22+
.accept(MediaType.APPLICATION_JSON)
23+
.retrieve()
24+
.body<T>().let {
25+
it ?: return HttpResponse.Error(500, "response body was null")
26+
return HttpResponse.Ok(200, it)
27+
}
28+
} catch (e: HttpClientErrorException) {
29+
HttpResponse.Error(e.statusCode.value(), e.responseBodyAsString)
30+
}
31+
32+
sealed class HttpResponse<out T> {
33+
data class Ok<T>(val statusCode: Int, val body: T) : HttpResponse<T>()
34+
data class Error(val statusCode: Int, val body: String) : HttpResponse<Nothing>()
35+
}
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
package gent.zeus.guitar
22

3-
import org.slf4j.Logger
43
import org.slf4j.LoggerFactory
4+
import org.slf4j.Logger
5+
56

6-
object Logging {
7-
val log: Logger = LoggerFactory.getLogger("GUITAR")
8-
}
7+
val logger: Logger = LoggerFactory.getLogger("guitar log")

src/main/kotlin/gent/zeus/guitar/StartupCheck.kt

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)