First public release
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details

pull/1/head 1.5.0
kalle 2022-07-04 12:08:40 +02:00
parent 93080246f6
commit 998c758df5
40 changed files with 1689 additions and 0 deletions

42
.drone.yml Normal file
View File

@ -0,0 +1,42 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: "Build"
image: "eclipse-temurin:17-jdk"
commands:
- "./gradlew assemble"
- name: "Test"
image: "eclipse-temurin:17-jdk"
commands:
- "./gradlew test"
- name: "Create Release"
image: plugins/gitea-release
settings:
api_key:
from_secret: "gitea_api_key"
base_url:
from_secret: "gitea_base_url"
files: build/libs/DLib-*.jar
when:
event:
- tag
- name: "Publish to Maven"
image: "eclipse-temurin:17-jdk"
commands:
- "./gradlew publishReleasePublicationToMavenRepository"
environment:
MAVEN_REPO_URL:
from_secret: maven_repo_url
MAVEN_REPO_TOKEN:
from_secret: gitea_api_key
when:
event:
- tag
trigger:
event:
- push
- pull_request
- tag

5
.gitignore vendored
View File

@ -1,3 +1,7 @@
build/
.idea/
.gradle/
# ---> Kotlin # ---> Kotlin
# Compiled class file # Compiled class file
*.class *.class
@ -23,3 +27,4 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid* hs_err_pid*
!gradle/wrapper/gradle-wrapper.jar

90
build.gradle.kts Normal file
View File

@ -0,0 +1,90 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.7.0"
kotlin("plugin.serialization") version "1.7.0"
id("maven-publish")
id("com.github.johnrengelman.shadow") version "7.1.2"
id("io.papermc.paperweight.userdev") version "1.3.5"
}
group = "nl.kallestruik"
version = "1.5.0"
repositories {
mavenCentral()
}
dependencies {
paperDevBundle("1.19-R0.1-SNAPSHOT")
implementation(kotlin("reflect"))
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
implementation("org.jetbrains.kotlin:kotlin-scripting-common")
implementation("org.jetbrains.kotlin:kotlin-scripting-jvm")
implementation("org.jetbrains.kotlin:kotlin-scripting-jvm-host")
testImplementation(kotlin("test-junit5"))
testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")
}
tasks {
build {
dependsOn(shadowJar)
}
assemble {
dependsOn(reobfJar)
}
test {
useJUnitPlatform()
}
withType<KotlinCompile> {
kotlinOptions.jvmTarget = "17"
}
processResources {
expand("version" to project.version)
}
}
val sourcesJar by tasks.creating(Jar::class) {
archiveClassifier.set("sources")
from(sourceSets["main"].allSource)
}
publishing {
publications {
create<MavenPublication>("release") {
from(components["kotlin"])
artifact(sourcesJar)
}
}
repositories {
maven {
url = uri(System.getenv("MAVEN_REPO_URL") ?: "")
credentials(HttpHeaderCredentials::class) {
name = "Authorization"
value = "token ${System.getenv("MAVEN_REPO_TOKEN")}"
}
authentication {
create<HttpHeaderAuthentication>("Authentication Header")
}
}
}
}
kotlin {
sourceSets {
all {
languageSettings.optIn("kotlin.RequiresOptIn")
languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi")
}
}
}

1
gradle.properties Normal file
View File

@ -0,0 +1 @@
kotlin.code.style=official

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Executable file
View File

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MSYS* | MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

9
settings.gradle.kts Normal file
View File

@ -0,0 +1,9 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven("https://papermc.io/repo/repository/maven-public/")
}
}
rootProject.name = "DLib"

View File

@ -0,0 +1,8 @@
package nl.kallestruik.dlib
import org.bukkit.plugin.java.JavaPlugin
/**
* This class is only here to make sure that paper loads the plugin.
*/
class DLib: JavaPlugin()

View File

@ -0,0 +1,45 @@
package nl.kallestruik.dlib
import org.bukkit.util.Vector
import java.util.*
class DUtil(
private val random: Random
) {
/**
* Create a random vector with integer offsets within the min and max.
* @param min The minimum offset.
* @param max The maximum offset.
* @param height Whether the height should also have a random offset.
* @return A [Vector] containing the offsets.
*/
fun getRandomLocationOffset(min: Int, max: Int, height: Boolean): Vector {
// Create the vectors with all positive numbers.
val vec = if (height) {
Vector(
min + random.nextInt(max - min),
min + random.nextInt(max - min),
min + random.nextInt(max - min)
)
} else {
Vector(
min + random.nextInt(max - min),
0,
min + random.nextInt(max - min)
)
}
// Randomly make some components negative instead of positive.
vec.multiply(
Vector(
if (random.nextBoolean()) -1 else 1,
if (random.nextBoolean()) -1 else 1,
if (random.nextBoolean()) -1 else 1
)
)
// Return the completed vector.
return vec
}
}

View File

@ -0,0 +1,26 @@
package nl.kallestruik.dlib
class MathHelper {
/**
* Clamp an integer between a min and a max value.
* @param value The value to clamp.
* @param min The minimal value.
* @param max The maximum value.
*/
fun clamp(value: Int, min: Int, max: Int): Int {
return if (value < min) min else value.coerceAtMost(max)
}
/**
* Force a value to between 0 15 (inclusive) wrapping around to 15 at negative values.
* @param value The value.
* @return A positive int between 0 and 15 (inclusive).
*/
fun chunkAbs(value: Int): Int {
return if (value >= 0)
value % 16
else
16 + (value % 16)
}
}

View File

@ -0,0 +1,37 @@
package nl.kallestruik.dlib
import java.lang.reflect.Field
class ReflectionUtil {
/**
* Get a {@link Field} from the class provided.
* @param clazz The class to get the field from.
* @param fieldName The name of the field to get.
*/
@Throws(NoSuchFieldException::class)
fun getField(clazz: Class<*>, fieldName: String): Field {
return try {
clazz.getDeclaredField(fieldName)
} catch (e: NoSuchFieldException) {
val superClass = clazz.superclass
superClass?.let { getField(it, fieldName) } ?: throw e
}
}
/**
* Get the value from a field on an object.
* @param instance The instance of the object to take the value from.
* @param fieldName The name of the field to take the value from.
*/
fun getValueFromField(instance: Any, fieldName: String): Any? {
try {
val field = getField(instance.javaClass, fieldName)
field.isAccessible = true //required if field is not normally accessible
return field.get(instance)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
}
return null
}
}

View File

@ -0,0 +1,223 @@
package nl.kallestruik.dlib.config
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.json.JsonPrimitive
import nl.kallestruik.dlib.config.annotations.*
import nl.kallestruik.dlib.config.data.ConfigSchema
import nl.kallestruik.dlib.config.data.ConfigValueConstraints
import nl.kallestruik.dlib.config.data.ConfigValueType
import nl.kallestruik.dlib.exceptions.ConfigSchemaGenerationException
import kotlin.math.min
import kotlin.math.max
fun SerialDescriptor.toConfigSchema(annotations: List<Annotation> = listOf()): ConfigSchema {
val allAnnotations = annotations + this.annotations
return when (this.kind) {
SerialKind.ENUM -> createEnumConfigSchema(this, allAnnotations)
PrimitiveKind.BOOLEAN -> createBooleanConfigSchema(allAnnotations)
PrimitiveKind.BYTE -> createIntegerConfigSchema(allAnnotations, Byte.MAX_VALUE.toLong(), Byte.MIN_VALUE.toLong())
PrimitiveKind.CHAR -> createStringConfigSchema(allAnnotations, maxLength=1)
PrimitiveKind.SHORT -> createIntegerConfigSchema(allAnnotations, Short.MAX_VALUE.toLong(), Short.MIN_VALUE.toLong())
PrimitiveKind.INT -> createIntegerConfigSchema(allAnnotations, Int.MAX_VALUE.toLong(), Int.MIN_VALUE.toLong())
PrimitiveKind.LONG -> createIntegerConfigSchema(allAnnotations, Long.MAX_VALUE, Long.MIN_VALUE)
PrimitiveKind.FLOAT -> createFloatConfigSchema(allAnnotations, Float.MAX_VALUE.toDouble(), -Float.MAX_VALUE.toDouble())
PrimitiveKind.DOUBLE -> createFloatConfigSchema(allAnnotations, Double.MAX_VALUE, -Double.MAX_VALUE)
PrimitiveKind.STRING -> createStringConfigSchema(allAnnotations)
StructureKind.LIST -> createListConfigSchema(this, allAnnotations)
StructureKind.MAP -> createMapConfigSchema(this, allAnnotations)
StructureKind.OBJECT, StructureKind.CLASS -> createObjectConfigSchema(this, allAnnotations)
else -> throw ConfigSchemaGenerationException("Creating a schema for type ${this.kind} is not currently supported.")
}
}
private fun createObjectConfigSchema(descriptor: SerialDescriptor, annotations: List<Annotation>): ConfigSchema {
val description = annotations
.filterIsInstance<Description>()
.flatMap { it.lines.toList() }
val children = descriptor.elementDescriptors
.mapIndexed { i, child ->
descriptor.getElementName(i) to child.toConfigSchema(descriptor.getElementAnnotations(i))
}.toMap()
return ConfigSchema(
type = ConfigValueType.OBJECT,
children = children,
description = description,
)
}
private fun createListConfigSchema(descriptor: SerialDescriptor, annotations: List<Annotation>): ConfigSchema {
val description = annotations
.filterIsInstance<Description>()
.flatMap { it.lines.toList() }
return ConfigSchema(
type = ConfigValueType.LIST,
description = description,
items = descriptor.elementDescriptors.first().toConfigSchema(),
)
}
private fun createMapConfigSchema(descriptor: SerialDescriptor, annotations: List<Annotation>): ConfigSchema {
val description = annotations
.filterIsInstance<Description>()
.flatMap { it.lines.toList() }
val allowedKeys = annotations
.filterIsInstance<AllowedKeys>()
.flatMap { annotation -> annotation.keys
.map { listOf(it) }
}
val allowedKeysExclusive = annotations
.filterIsInstance<AllowedKeysExclusive>()
.map { it.exclusiveKeys.toList() }
if (descriptor.elementDescriptors.first().kind != PrimitiveKind.STRING) {
throw ConfigSchemaGenerationException("Map keys have to be strings.")
}
return ConfigSchema(
type = ConfigValueType.MAP,
description = description,
items = descriptor.elementDescriptors.last().toConfigSchema(),
constraints = ConfigValueConstraints(
allowedKeys = allowedKeys + allowedKeysExclusive,
),
)
}
private fun createStringConfigSchema(annotations: List<Annotation>, maxLength: Int? = null): ConfigSchema {
val description = annotations
.filterIsInstance<Description>()
.flatMap { it.lines.toList() }
val allowedValues = annotations
.filterIsInstance<StringEnum>()
.flatMap { it.values.toList() }
val default = annotations
.filterIsInstance<DefaultString>()
.firstOrNull()
?.let { JsonPrimitive(it.default) }
return ConfigSchema(
type = ConfigValueType.STRING,
description = description,
default = default,
constraints = ConfigValueConstraints(
enum = allowedValues.ifEmpty { null },
maxLength = maxLength,
),
)
}
private fun createEnumConfigSchema(descriptor: SerialDescriptor, annotations: List<Annotation>): ConfigSchema {
val description = annotations
.filterIsInstance<Description>()
.flatMap { it.lines.toList() }
val allowedValues = descriptor.elementNames.toList()
val default = annotations
.filterIsInstance<DefaultString>()
.firstOrNull()
?.let { JsonPrimitive(it.default) }
return ConfigSchema(
type = ConfigValueType.STRING,
description = description,
default = default,
constraints = ConfigValueConstraints(
enum = allowedValues.ifEmpty { null },
),
)
}
private fun createBooleanConfigSchema(annotations: List<Annotation>): ConfigSchema {
val description = annotations
.filterIsInstance<Description>()
.flatMap { it.lines.toList() }
val default = annotations
.filterIsInstance<DefaultBoolean>()
.firstOrNull()
?.let { JsonPrimitive(it.default) }
return ConfigSchema(
type = ConfigValueType.BOOLEAN,
description = description,
default = default,
)
}
private fun createIntegerConfigSchema(annotations: List<Annotation>, maxValue: Long, minValue: Long): ConfigSchema {
val description = annotations
.filterIsInstance<Description>()
.flatMap { it.lines.toList() }
val max = annotations
.filterIsInstance<MaxInt>()
.map { it.max }
.reduceOrNull { acc, next ->
return@reduceOrNull max(acc, next)
} ?: maxValue
val min = annotations
.filterIsInstance<MinInt>()
.map { it.min }
.reduceOrNull { acc, next ->
min(acc, next)
} ?: minValue
val default = annotations
.filterIsInstance<DefaultInt>()
.firstOrNull()
?.let { JsonPrimitive(it.default) }
return ConfigSchema(
type = ConfigValueType.INTEGER,
description = description,
default = default,
constraints = ConfigValueConstraints(
maxValueInt = max,
minValueInt = min,
),
)
}
private fun createFloatConfigSchema(annotations: List<Annotation>, maxValue: Double, minValue: Double): ConfigSchema {
val description = annotations
.filterIsInstance<Description>()
.flatMap { it.lines.toList() }
val max = annotations
.filterIsInstance<MaxFloat>()
.map { it.max }
.reduceOrNull { acc, next ->
return@reduceOrNull max(acc, next)
} ?: maxValue
val min = annotations
.filterIsInstance<MinFloat>()
.map { it.min }
.reduceOrNull { acc, next ->
min(acc, next)
} ?: minValue
val default = annotations
.filterIsInstance<DefaultFloat>()
.firstOrNull()
?.let { JsonPrimitive(it.default) }
return ConfigSchema(
type = ConfigValueType.FLOAT,
description = description,
default = default,
constraints = ConfigValueConstraints(
maxValueFloat = max,
minValueFloat = min,
),
)
}

View File

@ -0,0 +1,11 @@
package nl.kallestruik.dlib.config.annotations
import kotlinx.serialization.SerialInfo
@SerialInfo
@Repeatable
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
annotation class AllowedKeys(
vararg val keys: String,
)

View File

@ -0,0 +1,11 @@
package nl.kallestruik.dlib.config.annotations
import kotlinx.serialization.SerialInfo
@SerialInfo
@Repeatable
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
annotation class AllowedKeysExclusive(
vararg val exclusiveKeys: String,
)

View File

@ -0,0 +1,10 @@
package nl.kallestruik.dlib.config.annotations
import kotlinx.serialization.SerialInfo
@SerialInfo
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
annotation class DefaultBoolean(
val default: Boolean,
)

View File

@ -0,0 +1,10 @@
package nl.kallestruik.dlib.config.annotations
import kotlinx.serialization.SerialInfo
@SerialInfo
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
annotation class DefaultFloat(
val default: Double,
)

View File

@ -0,0 +1,10 @@
package nl.kallestruik.dlib.config.annotations
import kotlinx.serialization.SerialInfo
@SerialInfo
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
annotation class DefaultInt(
val default: Long,
)

View File

@ -0,0 +1,10 @@
package nl.kallestruik.dlib.config.annotations
import kotlinx.serialization.SerialInfo
@SerialInfo
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
annotation class DefaultString(
val default: String,
)

View File

@ -0,0 +1,11 @@
package nl.kallestruik.dlib.config.annotations
import kotlinx.serialization.SerialInfo
@SerialInfo
@Repeatable
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
annotation class Description(
vararg val lines: String,
)

View File

@ -0,0 +1,11 @@
package nl.kallestruik.dlib.config.annotations
import kotlinx.serialization.SerialInfo
import nl.kallestruik.dlib.config.data.ConfigDisplayType
@SerialInfo
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
annotation class DisplayType(
val type: ConfigDisplayType
)

View File

@ -0,0 +1,10 @@
package nl.kallestruik.dlib.config.annotations
import kotlinx.serialization.SerialInfo
@SerialInfo
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
annotation class MaterialField(
val field: String,
)

View File

@ -0,0 +1,10 @@
package nl.kallestruik.dlib.config.annotations
import kotlinx.serialization.SerialInfo
@SerialInfo
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
annotation class MaxFloat(
val max: Double,
)

View File

@ -0,0 +1,10 @@
package nl.kallestruik.dlib.config.annotations
import kotlinx.serialization.SerialInfo
@SerialInfo
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
annotation class MaxInt(
val max: Long,
)

View File

@ -0,0 +1,10 @@
package nl.kallestruik.dlib.config.annotations
import kotlinx.serialization.SerialInfo
@SerialInfo
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
annotation class MinFloat(
val min: Double,
)

View File

@ -0,0 +1,10 @@
package nl.kallestruik.dlib.config.annotations
import kotlinx.serialization.SerialInfo
@SerialInfo
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
annotation class MinInt(
val min: Long,
)

View File

@ -0,0 +1,11 @@
package nl.kallestruik.dlib.config.annotations
import kotlinx.serialization.SerialInfo
import org.bukkit.Material
@SerialInfo
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
annotation class StaticMaterial(
val material: Material,
)

View File

@ -0,0 +1,11 @@
package nl.kallestruik.dlib.config.annotations
import kotlinx.serialization.SerialInfo
@SerialInfo
@Repeatable
@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
annotation class StringEnum(
vararg val values: String,
)

View File

@ -0,0 +1,20 @@
package nl.kallestruik.dlib.config.data
import kotlinx.serialization.Serializable
@Serializable
data class ConfigDisplaySettings(
val type: ConfigDisplayType = ConfigDisplayType.DEFAULT,
val materialField: String? = null,
val staticMaterial: String? = null,
val amountField: String? = null,
val staticAmount: Int? = null,
val inventorySize: Int? = null,
)
enum class ConfigDisplayType {
DEFAULT,
CHECKLIST,
ITEM,
INVENTORY,
}

View File

@ -0,0 +1,15 @@
package nl.kallestruik.dlib.config.data
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive
@Serializable
data class ConfigSchema(
val type: ConfigValueType,
val description: List<String>? = null,
val children: Map<String, ConfigSchema>? = null,
val items: ConfigSchema? = null,
val default: JsonPrimitive? = null,
val constraints: ConfigValueConstraints = ConfigValueConstraints(),
val display: ConfigDisplaySettings? = null,
)

View File

@ -0,0 +1,14 @@
package nl.kallestruik.dlib.config.data
import kotlinx.serialization.Serializable
@Serializable
data class ConfigValueConstraints(
val enum: List<String>? = null,
val allowedKeys: List<List<String>>? = null,
val maxLength: Int? = null,
val maxValueInt: Long? = null,
val minValueInt: Long? = null,
val maxValueFloat: Double? = null,
val minValueFloat: Double? = null,
)

View File

@ -0,0 +1,11 @@
package nl.kallestruik.dlib.config.data
enum class ConfigValueType {
STRING,
MAP,
LIST,
OBJECT,
BOOLEAN,
INTEGER,
FLOAT
}

View File

@ -0,0 +1,3 @@
package nl.kallestruik.dlib.exceptions
class ConfigSchemaGenerationException(message: String = ""): RuntimeException(message)

View File

@ -0,0 +1,222 @@
package nl.kallestruik.dlib.gui
import kotlinx.coroutines.runBlocking
import net.kyori.adventure.text.Component
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import org.bukkit.event.HandlerList
import org.bukkit.event.Listener
import org.bukkit.event.inventory.ClickType
import org.bukkit.event.inventory.InventoryAction
import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.event.inventory.InventoryCloseEvent
import org.bukkit.inventory.InventoryView
import org.bukkit.inventory.ItemStack
import org.bukkit.plugin.Plugin
fun chestGUI(player: Player, plugin: Plugin, configure: suspend ChestGUI.() -> Unit) {
val chestGUI = ChestGUI()
runBlocking {
chestGUI.configure()
}
chestGUI.display(player, plugin)
}
class ChestGUI: Listener {
private data class Configuration(
var border: (suspend ChestGUIEntry.() -> Unit)? = null,
var content: MutableList<suspend ChestGUIEntry.() -> Unit> = mutableListOf(),
var itemStacks: MutableMap<Int, ItemStack> = mutableMapOf(),
var onClose: suspend (gui: ChestGUI, player: Player, event: InventoryCloseEvent) -> Unit = { _, _, _ ->}
)
private val configuration = Configuration()
var title: Component = Component.empty()
var rows: Int = 0
var cancelInteraction: Boolean = true
private lateinit var view: InventoryView
private lateinit var entries: Array<ChestGUIEntry?>
fun onClose(onClose: suspend (gui: ChestGUI, player: Player, event: InventoryCloseEvent) -> Unit) {
configuration.onClose = onClose
}
fun entry(entry: suspend ChestGUIEntry.() -> Unit) {
configuration.content.add(entry)
}
fun border(border: suspend ChestGUIEntry.() -> Unit) {
configuration.border = border
}
fun itemStack(slot: Int, itemStack: ItemStack) {
configuration.itemStacks[slot] = itemStack
}
fun display(player: Player, plugin: Plugin) {
val inventory = Bukkit.createInventory(null, rows * 9, title)
view = player.openInventory(inventory) ?: return
entries = arrayOfNulls(rows * 9)
if (configuration.border != null) {
val border = ChestGUIEntry()
runBlocking {
configuration.border!!.invoke(border)
}
val borderStack = border.toItemStack()
// Top/Bottom border
for (x in 0..8) {
view.setItem(x, borderStack)
view.setItem((rows - 1) * 9 + x, borderStack)
}
// Middle side border
for (y in 1..rows - 2) {
view.setItem(9 * y, borderStack)
view.setItem(9 * y + 8, borderStack)
}
}
configuration.content.forEach {
val entry = ChestGUIEntry()
runBlocking {
it.invoke(entry)
}
if (entry.getSlot() < 0)
return@forEach
entries[entry.getSlot()] = entry
}
entries.forEach { entry ->
if (entry == null)
return@forEach
if (entry.x != -1 || entry.y != -1)
return@forEach
inventory.setItem(entry.getSlot(), entry.toItemStack())
}
configuration.itemStacks.forEach { (slot, itemStack) ->
inventory.setItem(slot, itemStack)
}
plugin.server.pluginManager.registerEvents(this, plugin)
}
private fun cleanup() {
HandlerList.unregisterAll(this)
}
/*
Bukkit event handlers
*/
@EventHandler
fun onInventoryClose(event: InventoryCloseEvent) {
if (event.view != this.view)
return
cleanup()
if (event.reason == InventoryCloseEvent.Reason.PLUGIN) {
return
}
if (event.player !is Player)
return
runBlocking {
configuration.onClose(this@ChestGUI, event.player as Player, event)
}
}
@EventHandler
fun onClick(event: InventoryClickEvent) {
if (event.view != view) {
return
}
if (event.action != InventoryAction.PICKUP_ALL) {
if (cancelInteraction)
event.isCancelled = true
return
}
val slot = view.convertSlot(event.rawSlot)
if (cancelInteraction)
event.isCancelled = true
val entry = entries[slot] ?: return
if (event.whoClicked !is Player)
return
runBlocking {
when (event.click) {
ClickType.LEFT -> entry.onLeftClick(event)
ClickType.MIDDLE -> entry.onMiddleClick(event)
ClickType.RIGHT -> entry.onRightClick(event)
else -> entry.onOtherClick(event)
}
}
}
}
data class ChestGUIEntry(
var name: Component = Component.empty(),
var material: Material = Material.AIR,
var amount: Int = 1,
var lore: MutableList<Component> = mutableListOf(),
var x: Int = -1,
var y: Int = -1,
var onLeftClick: suspend (event: InventoryClickEvent) -> Unit = {},
var onMiddleClick: suspend (event: InventoryClickEvent) -> Unit = {},
var onRightClick: suspend (event: InventoryClickEvent) -> Unit = {},
var onOtherClick: suspend (event: InventoryClickEvent) -> Unit = {},
) {
fun onLeftClick(onLeftClick: suspend (event: InventoryClickEvent) -> Unit) {
this.onLeftClick = onLeftClick
}
fun onMiddleClick(onMiddleClick: suspend (event: InventoryClickEvent) -> Unit) {
this.onMiddleClick = onMiddleClick
}
fun onRightClick(onRightClick: suspend (event: InventoryClickEvent) -> Unit) {
this.onRightClick = onRightClick
}
fun onOtherClick(onOtherClick: suspend (event: InventoryClickEvent) -> Unit) {
this.onOtherClick = onOtherClick
}
fun lore(line: Component) {
lore.add(line)
}
fun getSlot(): Int {
return x + y * 9
}
fun toItemStack(): ItemStack {
val itemStack = ItemStack(material, amount)
val itemMeta = itemStack.itemMeta
itemMeta.displayName(name)
itemMeta.lore(lore)
itemStack.itemMeta = itemMeta
return itemStack
}
}

View File

@ -0,0 +1,159 @@
package nl.kallestruik.dlib.gui
import kotlinx.coroutines.runBlocking
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
import org.bukkit.Bukkit
import org.bukkit.Material
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import org.bukkit.event.HandlerList
import org.bukkit.event.Listener
import org.bukkit.event.inventory.InventoryAction
import org.bukkit.event.inventory.InventoryClickEvent
import org.bukkit.event.inventory.InventoryCloseEvent
import org.bukkit.inventory.InventoryView
import org.bukkit.inventory.ItemStack
import org.bukkit.plugin.Plugin
fun confirmationDialog(player: Player, plugin: Plugin, configure: suspend ConfirmationDialog.() -> Unit) {
val confirmationDialog = ConfirmationDialog()
runBlocking {
confirmationDialog.configure()
}
confirmationDialog.display(player, plugin)
}
class ConfirmationDialog: Listener {
private data class Configuration(
var onConfirm: suspend () -> Unit = {},
var onCancel: suspend () -> Unit = {}
)
var title: Component = Component.empty()
var description: Component = Component.empty()
private val configuration = Configuration()
private lateinit var plugin: Plugin
private lateinit var view: InventoryView
fun onConfirm(onConfirm: suspend () -> Unit) {
configuration.onConfirm = onConfirm
}
fun onCancel(onCancel: suspend () -> Unit) {
configuration.onCancel = onCancel
}
fun display(player: Player, plugin: Plugin) {
player.closeInventory()
this.plugin = plugin
val inventory = Bukkit.createInventory(null, 3 * 9, title)
view = player.openInventory(inventory) ?: return
val borderStack = ItemStack(Material.ORANGE_STAINED_GLASS_PANE, 1)
val borderMeta = borderStack.itemMeta
borderMeta.displayName(Component.empty())
borderStack.itemMeta = borderMeta
// Top border
for (x in 8 downTo 0) {
view.setItem(x, borderStack)
}
// Middle sides
view.setItem(9, borderStack)
view.setItem(17, borderStack)
// Bottom border
for (x in 26 downTo 18) {
view.setItem(x, borderStack)
}
// Description item
val descriptionItem = ItemStack(Material.PAPER, 1)
val descriptionMeta = descriptionItem.itemMeta
descriptionMeta.displayName(description)
descriptionItem.itemMeta = descriptionMeta
// Confirm item
val confirmItem = ItemStack(Material.SUNFLOWER, 1)
val confirmMeta = confirmItem.itemMeta
confirmMeta.displayName(Component.text("Confirm").color(NamedTextColor.GREEN))
confirmItem.itemMeta = confirmMeta
// Cancel item
val cancelItem = ItemStack(Material.BARRIER, 1)
val cancelMeta = cancelItem.itemMeta
cancelMeta.displayName(Component.text("Cancel").color(NamedTextColor.RED))
cancelItem.itemMeta = cancelMeta
// 0 1 2 3 4 5 6 7 8
// 9 10 11 12 13 14 15 16 17
// 18 19 20 21 22 23 24 25 26
view.setItem(4, descriptionItem)
view.setItem(11, confirmItem)
view.setItem(15, cancelItem)
plugin.server.pluginManager.registerEvents(this, plugin)
}
private fun cleanup() {
view.close()
HandlerList.unregisterAll(this)
}
/*
Bukkit event handlers
*/
@EventHandler
fun onInventoryClose(event: InventoryCloseEvent) {
if (event.view != this.view)
return
if (event.reason == InventoryCloseEvent.Reason.PLUGIN) {
return
}
runBlocking {
configuration.onCancel.invoke()
cleanup()
}
}
@EventHandler
fun onOperationFinish(event: InventoryClickEvent) {
if (event.view != view) {
return
}
if (event.action != InventoryAction.PICKUP_ALL) {
event.isCancelled = true
return
}
val slot = view.convertSlot(event.rawSlot)
if (slot != 11 && slot != 15) {
event.isCancelled = true
return
}
event.isCancelled = true
if (slot == 11) {
runBlocking {
configuration.onConfirm()
cleanup()
}
} else {
runBlocking {
configuration.onCancel()
cleanup()
}
}
}
}

View File

@ -0,0 +1,138 @@
package nl.kallestruik.dlib.gui
import kotlinx.coroutines.runBlocking
import net.kyori.adventure.text.Component
import org.bukkit.Material
import org.bukkit.entity.Player
import org.bukkit.event.EventHandler
import org.bukkit.event.HandlerList
import org.bukkit.event.Listener
import org.bukkit.event.inventory.*
import org.bukkit.inventory.InventoryView
import org.bukkit.inventory.ItemStack
import org.bukkit.plugin.Plugin
fun textInputDialog(player: Player, plugin: Plugin, configure: suspend TextInputDialog.() -> Unit) {
val textInputDialog = TextInputDialog()
runBlocking {
textInputDialog.configure()
}
textInputDialog.display(player, plugin)
}
class TextInputDialog: Listener {
private data class Configuration(
var item: suspend TextInputDialogItem.() -> Unit = {},
var onFinish: suspend (value: String) -> Unit = {},
var onCancel: suspend () -> Unit = {}
)
private val configuration = Configuration()
var prompt: Component = Component.empty()
private lateinit var view: InventoryView
private lateinit var plugin: Plugin
fun item(entry: suspend TextInputDialogItem.() -> Unit) {
configuration.item = entry
}
fun onFinish(onFinish: suspend (value: String) -> Unit) {
configuration.onFinish = onFinish
}
fun onCancel(onCancel: suspend () -> Unit) {
configuration.onCancel = onCancel
}
fun display(player: Player, plugin: Plugin) {
player.closeInventory()
this.plugin = plugin
view = player.openAnvil(null, true) ?: return
val entry = TextInputDialogItem()
runBlocking {
configuration.item.invoke(entry)
}
val itemStack = ItemStack(entry.material, entry.amount)
val itemMeta = itemStack.itemMeta
itemMeta.displayName(prompt)
itemStack.itemMeta = itemMeta
view.setItem(0, itemStack)
plugin.server.pluginManager.registerEvents(this, plugin)
}
private fun cleanup() {
view.close()
HandlerList.unregisterAll(this)
}
/*
Bukkit event handlers
*/
@EventHandler
fun onInventoryClose(event: InventoryCloseEvent) {
if (event.view != this.view)
return
if (event.reason == InventoryCloseEvent.Reason.PLUGIN) {
return
}
runBlocking {
configuration.onCancel.invoke()
cleanup()
}
}
@EventHandler
fun onAnvilPrepare(event: PrepareAnvilEvent) {
if (event.view != view)
return
// Always keep the cost at 1. 0 Causes the client to not be able to click it.
event.inventory.repairCost = 1
}
@EventHandler
fun onOperationFinish(event: InventoryClickEvent) {
if (event.view != view) {
return
}
if (event.action != InventoryAction.PICKUP_ALL) {
event.isCancelled = true
return
}
if (view.convertSlot(event.rawSlot) != 2) {
event.isCancelled = true
return
}
val item = event.currentItem
val name = item?.itemMeta?.displayName ?: ""
if (name == "") {
event.isCancelled = true
return
}
event.isCancelled = true
runBlocking {
configuration.onFinish.invoke(name)
cleanup()
}
}
}
data class TextInputDialogItem(
var material: Material = Material.AIR,
var amount: Int = 1,
)

View File

@ -0,0 +1,21 @@
package nl.kallestruik.dlib.serializers
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.util.*
object UUIDSerializer: KSerializer<UUID> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("uuid", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): UUID {
return UUID.fromString(decoder.decodeString())
}
override fun serialize(encoder: Encoder, value: UUID) {
encoder.encodeString(value.toString())
}
}

View File

@ -0,0 +1,4 @@
name: "DLib"
main: nl.kallestruik.dlib.DLib
version: ${version}
api-version: 1.16

View File

@ -0,0 +1,161 @@
package nl.kallestruik.dlib.config
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive
import nl.kallestruik.dlib.config.annotations.*
import kotlin.test.Test
import kotlin.test.assertEquals
class ConfigSchemaBuilderTest {
@Test
fun `config scheme contains all descriptions`() {
val schema = Company.serializer().descriptor.toConfigSchema()
assertEquals(
listOf("The name of the company."),
schema.children!!["name"]!!.description
)
assertEquals(
listOf("The person that owns the company."),
schema.children!!["owner"]!!.description
)
assertEquals(
listOf("All the employees of the company.", "This list should also contain the owner for legacy reasons ;)."),
schema.children!!["employees"]!!.description
)
val personSchema = schema.children!!["owner"]!!
assertEquals(
listOf("The list of qualifications that this person has."),
personSchema.children!!["qualifications"]!!.description
)
assertEquals(
listOf("The age of the person."),
personSchema.children!!["age"]!!.description
)
assertEquals(
listOf("Whether the person is currently employed at a company."),
personSchema.children!!["isEmployed"]!!.description
)
}
@Test
fun `config schema has correct defaults`() {
val schema = Company.serializer().descriptor.toConfigSchema()
val personSchema = schema.children!!["owner"]!!
assertEquals(JsonPrimitive("Bob"), personSchema.children!!["firstName"]!!.default)
assertEquals(JsonPrimitive(18.0), personSchema.children!!["age"]!!.default)
assertEquals(JsonPrimitive(true), personSchema.children!!["isEmployed"]!!.default)
val qualificationSchema = personSchema.children!!["qualifications"]!!.items!!
assertEquals(JsonPrimitive("EXPERIENCE"), qualificationSchema.children!!["type"]!!.default)
assertEquals(JsonPrimitive(0), qualificationSchema.children!!["yearObtained"]!!.default)
}
@Test
fun `config schema allowed keys are correct`() {
val schema = Company.serializer().descriptor.toConfigSchema()
assertEquals(
listOf(listOf("Bob"), listOf("Joe"), listOf("Kon"), listOf("Jane", "Kevin")),
schema.children!!["employees"]!!.constraints.allowedKeys
)
}
@Test
fun `config schema enum values are correct`() {
val schema = Company.serializer().descriptor.toConfigSchema()
val personSchema = schema.children!!["owner"]!!
assertEquals(
listOf("Bob", "Joe", "Kon", "Jane", "Kevin"),
personSchema.children!!["firstName"]!!.constraints.enum
)
val qualificationSchema = personSchema.children!!["qualifications"]!!.items!!
assertEquals(
listOf("DIPLOMA", "EXPERIENCE", "CERTIFICATE"),
qualificationSchema.children!!["type"]!!.constraints.enum
)
}
@Test
fun `config schema has correct min and max values`() {
val schema = Company.serializer().descriptor.toConfigSchema()
val personSchema = schema.children!!["owner"]!!
assertEquals(
0.0,
personSchema.children!!["age"]!!.constraints.minValueFloat
)
assertEquals(
75.0,
personSchema.children!!["age"]!!.constraints.maxValueFloat
)
val qualificationSchema = personSchema.children!!["qualifications"]!!.items!!
assertEquals(
0,
qualificationSchema.children!!["yearObtained"]!!.constraints.minValueInt
)
assertEquals(
9999,
qualificationSchema.children!!["yearObtained"]!!.constraints.maxValueInt
)
}
}
@Serializable
data class Company(
@Description("The name of the company.")
val name: String,
@Description("The person that owns the company.")
val owner: Person,
@Description("All the employees of the company.", "This list should also contain the owner for legacy reasons ;).")
@AllowedKeys("Bob", "Joe", "Kon") // Don't question it. I need to test stuff.
@AllowedKeysExclusive("Jane", "Kevin")
val employees: Map<String, Person>,
)
@Serializable
data class Person(
@DefaultString("Bob")
@StringEnum("Bob", "Joe", "Kon", "Jane", "Kevin")
val firstName: String,
val lastName: String,
@Description("The list of qualifications that this person has.")
val qualifications: List<Qualification>,
@DefaultFloat(18.0)
@MinFloat(0.0)
@MaxFloat(75.0)
@Description("The age of the person.")
val age: Float,
@DefaultBoolean(true)
@Description("Whether the person is currently employed at a company.")
val isEmployed: Boolean,
)
@Serializable
data class Qualification(
val name: String,
@DefaultString("EXPERIENCE")
val type: QualificationType,
@DefaultInt(0)
@MinInt(0)
@MaxInt(9999)
val yearObtained: Int,
)
@Serializable
enum class QualificationType {
DIPLOMA,
EXPERIENCE,
CERTIFICATE,
}