Skip to content

Commit

Permalink
move functions into Construct class to not use top-level functions, h…
Browse files Browse the repository at this point in the history
…opefully fixes issues
  • Loading branch information
testersen committed Nov 7, 2023
1 parent 057d037 commit d543911
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 163 deletions.
168 changes: 168 additions & 0 deletions src/main/kotlin/no/telenor/kt/env/Construct.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package no.telenor.kt.env

import no.telenor.kt.env.internal.ParserCollection
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.KType
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.functions
import kotlin.reflect.jvm.jvmErasure
import kotlin.reflect.jvm.jvmName

typealias ParserFn = (type: KType, name: String, value: String) -> Any?

class Construct {
companion object {
fun scan(classLoader: ClassLoader) {
ParserCollection.scan(classLoader)
}

private fun <T : Any> getAnnotatedConstructor(klass: KClass<T>): Pair<KFunction<T>, EnvConstructor> {
var constructor: KFunction<T>? = null
var envConstructor: EnvConstructor? = null

if (klass.typeParameters.isNotEmpty()) throw Exception("${klass.jvmName} cannot be constructed with environment variables, class has type parameters")

for (constr in klass.constructors) {
val envConstrAnnot = constr.annotations.find {
it is EnvConstructor
} ?: continue

if (constructor != null) {
throw Exception("${klass.jvmName} cannot be constructed with environment variables, class has more than 1 environmental constructor")
}

constructor = constr
envConstructor = envConstrAnnot as EnvConstructor
}

if (constructor == null) {
throw Exception("${klass.jvmName} cannot be constructed with environment variables, no @EnvConstructor annotation")
}

return (constructor to envConstructor!!)
}

private val lowerToUpper = Regex("(([a-z][A-Z])|([A-Z]{2}[a-z]))")
private val lowerToUpperTransform: (MatchResult) -> CharSequence = {
(it.value.substring(0, 1) + "_" + it.value.substring(1)).lowercase()
}

private fun transformNameToUpperSnakeCase(name: String): String {
return name.replace(lowerToUpper, lowerToUpperTransform).uppercase()
}

fun <T : Any> from(klass: KClass<T>): T {
val (constructor, envConstructorAnnot) = getAnnotatedConstructor(klass)
val values = mutableMapOf<KParameter, Any?>()
val problems = mutableListOf<Triple<KParameter, String?, Throwable>>()

for (parameter in constructor.parameters) {
val envAnnot = parameter.findAnnotation<Env>()
val optional = parameter.isOptional
val nullable = parameter.type.isMarkedNullable

if (envAnnot == null) {
if (optional) {
continue
}
if (nullable) {
values[parameter] = null
continue
}
problems.add(
Triple(
parameter,
null,
Throwable("not annotated with @Env, parameter is not optional or nullable")
)
)
continue
}

val parameterEnvNamePart =
envAnnot.value.ifBlank { null } ?: parameter.name?.let { transformNameToUpperSnakeCase(it) }

if (parameterEnvNamePart == null) {
problems.add(Triple(parameter, null, Throwable("name not inferrable")))
continue
}

val environmentVariableName = "${
if (envAnnot.noPrefix) "" else envConstructorAnnot.prefix
}$parameterEnvNamePart${
if (envAnnot.noSuffix) "" else envConstructorAnnot.suffix
}"

val environmentVariableValue = System.getenv(environmentVariableName)

if (environmentVariableValue == null || environmentVariableValue.isBlank()) {
if (optional) {
continue
} else if (nullable) {
values[parameter] = null
} else {
problems.add(
Triple(
parameter,
environmentVariableName,
Throwable("parameter cannot be null, no value from environment variable")
)
)
}
continue
}

val value = try {
parseValue(parameter.type, environmentVariableName, environmentVariableValue)
} catch (ex: Throwable) {
problems.add(Triple(parameter, environmentVariableName, ex))
continue
}

if (value == null) {
if (optional) {
continue
} else if (nullable) {
values[parameter] = null
} else {
problems.add(
Triple(
parameter,
environmentVariableName,
Throwable("parameter cannot be null, parser returned null")
)
)
}
continue
}

values[parameter] = value
}

if (problems.isNotEmpty()) throw EnvConstructException(klass, constructor, problems)

return constructor.callBy(values)
}

inline fun <reified T : Any> from() = from(T::class)

fun parseValue(type: KType, name: String, value: String): Any? {
if (type.jvmErasure.java.isEnum) {
return parseEnum(type, name, value)
}
val parser = ParserCollection.parsers[type.jvmErasure]
?: throw Throwable("No parser for '${type.jvmErasure}' was found!")
return parser(type, name, value)
}

fun parseEnum(
type: KType,
@Suppress("UNUSED_PARAMETER") name: String,
value: String
): Any? {
return type.jvmErasure.functions.first { it.name == "valueOf" }.call(value)
}
}
}
131 changes: 0 additions & 131 deletions src/main/kotlin/no/telenor/kt/env/construct.kt

This file was deleted.

25 changes: 0 additions & 25 deletions src/main/kotlin/no/telenor/kt/env/parseValue.kt

This file was deleted.

7 changes: 0 additions & 7 deletions src/main/kotlin/no/telenor/kt/env/parsers/mod.kt

This file was deleted.

0 comments on commit d543911

Please sign in to comment.