Skip to content

Commit c524d3c

Browse files
authored
Add validation for string fields from schemas (#9)
* Start adding validation from schemas * Add Pattern annotation and helper methods to get with validation * Add fabric mod id and icon patterns * fix typos * Add velocity plugin id validation * remove redundant parameter from orNullValidating
1 parent 1bfcb06 commit c524d3c

File tree

7 files changed

+97
-11
lines changed

7 files changed

+97
-11
lines changed

plugin/src/main/kotlin/xyz/jpenilla/resourcefactory/bukkit/BukkitPluginYml.kt

+6-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import org.spongepowered.configurate.objectmapping.ConfigSerializable
1616
import org.spongepowered.configurate.yaml.NodeStyle
1717
import xyz.jpenilla.resourcefactory.ConfigurateSingleFileResourceFactory
1818
import xyz.jpenilla.resourcefactory.ResourceFactory
19+
import xyz.jpenilla.resourcefactory.util.Pattern
1920
import xyz.jpenilla.resourcefactory.util.ProjectMetaConventions
21+
import xyz.jpenilla.resourcefactory.util.getValidating
2022
import xyz.jpenilla.resourcefactory.util.nullAction
2123
import xyz.jpenilla.resourcefactory.util.nullIfEmpty
2224

@@ -36,12 +38,14 @@ class BukkitPluginYml(
3638
@get:Optional
3739
val apiVersion: Property<String> = objects.property()
3840

41+
@Pattern("^[A-Za-z0-9_\\.-]+$", "Bukkit plugin name")
3942
@get:Input
4043
val name: Property<String> = objects.property()
4144

4245
@get:Input
4346
val version: Property<String> = objects.property()
4447

48+
@Pattern("^(?!org\\.bukkit\\.)([a-zA-Z_$][a-zA-Z\\d_$]*\\.)*[a-zA-Z_$][a-zA-Z\\d_$]*$", "Bukkit plugin main class name")
4549
@get:Input
4650
val main: Property<String> = objects.property()
4751

@@ -166,9 +170,9 @@ class BukkitPluginYml(
166170
@ConfigSerializable
167171
class Serializable(yml: BukkitPluginYml) {
168172
val apiVersion = yml.apiVersion.orNull
169-
val name = yml.name.get()
173+
val name = yml::name.getValidating()
170174
val version = yml.version.get()
171-
val main = yml.main.get()
175+
val main = yml::main.getValidating()
172176
val description = yml.description.orNull
173177
val load = yml.load.orNull
174178
val author = yml.author.orNull

plugin/src/main/kotlin/xyz/jpenilla/resourcefactory/bungee/BungeePluginYml.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import org.spongepowered.configurate.objectmapping.ConfigSerializable
1414
import org.spongepowered.configurate.yaml.NodeStyle
1515
import xyz.jpenilla.resourcefactory.ConfigurateSingleFileResourceFactory
1616
import xyz.jpenilla.resourcefactory.ResourceFactory
17+
import xyz.jpenilla.resourcefactory.util.Pattern
1718
import xyz.jpenilla.resourcefactory.util.ProjectMetaConventions
19+
import xyz.jpenilla.resourcefactory.util.getValidating
1820
import xyz.jpenilla.resourcefactory.util.nullAction
1921
import xyz.jpenilla.resourcefactory.util.nullIfEmpty
2022

@@ -30,6 +32,7 @@ class BungeePluginYml constructor(
3032
private val objects: ObjectFactory
3133
) : ConfigurateSingleFileResourceFactory.ObjectMapper.ValueProvider, ProjectMetaConventions, ResourceFactory.Provider {
3234

35+
@Pattern("^[A-Za-z0-9_\\.-]+$", "Bungee plugin name")
3336
@get:Input
3437
val name: Property<String> = objects.property()
3538

@@ -74,7 +77,7 @@ class BungeePluginYml constructor(
7477

7578
@ConfigSerializable
7679
class Serializable(yml: BungeePluginYml) {
77-
val name = yml.name.get()
80+
val name = yml::name.getValidating()
7881
val main = yml.main.get()
7982
val version = yml.version.orNull
8083
val author = yml.author.orNull

plugin/src/main/kotlin/xyz/jpenilla/resourcefactory/fabric/FabricModJson.kt

+12-2
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ import org.spongepowered.configurate.serialize.TypeSerializer
2020
import org.spongepowered.configurate.util.NamingSchemes
2121
import xyz.jpenilla.resourcefactory.ConfigurateSingleFileResourceFactory
2222
import xyz.jpenilla.resourcefactory.ResourceFactory
23+
import xyz.jpenilla.resourcefactory.util.Pattern
2324
import xyz.jpenilla.resourcefactory.util.ProjectMetaConventions
25+
import xyz.jpenilla.resourcefactory.util.getValidating
2426
import xyz.jpenilla.resourcefactory.util.nullAction
2527
import xyz.jpenilla.resourcefactory.util.nullIfEmpty
28+
import xyz.jpenilla.resourcefactory.util.validate
2629
import java.lang.reflect.Type
2730
import javax.inject.Inject
2831

@@ -38,6 +41,7 @@ open class FabricModJson constructor(
3841
private val objects: ObjectFactory
3942
) : ConfigurateSingleFileResourceFactory.ObjectMapper.ValueProvider, ProjectMetaConventions, ResourceFactory.Provider {
4043

44+
@Pattern("^[a-z][a-z0-9-_]{1,63}$", "Fabric mod id")
4145
@get:Input
4246
val id: Property<String> = objects.property()
4347

@@ -289,7 +293,7 @@ open class FabricModJson constructor(
289293
@ConfigSerializable
290294
open class Serializable(fmj: FabricModJson) {
291295
val schemaVersion = 1
292-
val id = fmj.id.get()
296+
val id = fmj::id.getValidating()
293297
val version = fmj.version.get()
294298
val environment = fmj.environment.orNull
295299
val entrypoints = fmj.entrypoints.get().groupBy({ it.type.get() }) {
@@ -309,7 +313,13 @@ open class FabricModJson constructor(
309313
val contributors = fmj.contributors.nullIfEmpty()?.map { SerializablePerson(it.name.get(), it.contact.asMap()) }
310314
val contact = fmj.contact.asMap()
311315
val license = fmj.license.nullIfEmpty()
312-
val icon = fmj.icon.orNull
316+
val icon = fmj.icon.orNull?.also {
317+
if (it is IconMap) {
318+
for (key in it.icons.keys) {
319+
key.validate("^[1-9][0-9]*$", "Icon key")
320+
}
321+
}
322+
}
313323
}
314324

315325
@ConfigSerializable

plugin/src/main/kotlin/xyz/jpenilla/resourcefactory/paper/PaperPluginYml.kt

+14-4
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ import org.spongepowered.configurate.yaml.NodeStyle
2020
import xyz.jpenilla.resourcefactory.ConfigurateSingleFileResourceFactory
2121
import xyz.jpenilla.resourcefactory.ResourceFactory
2222
import xyz.jpenilla.resourcefactory.bukkit.Permission
23+
import xyz.jpenilla.resourcefactory.util.Pattern
2324
import xyz.jpenilla.resourcefactory.util.ProjectMetaConventions
25+
import xyz.jpenilla.resourcefactory.util.getValidating
2426
import xyz.jpenilla.resourcefactory.util.nullAction
2527
import xyz.jpenilla.resourcefactory.util.nullIfEmpty
28+
import xyz.jpenilla.resourcefactory.util.orNullValidating
2629
import javax.inject.Inject
2730

2831
fun Project.paperPluginYml(configure: Action<PaperPluginYml> = nullAction()): PaperPluginYml {
@@ -36,23 +39,30 @@ class PaperPluginYml constructor(
3639
@Transient
3740
private val objects: ObjectFactory
3841
) : ConfigurateSingleFileResourceFactory.ObjectMapper.ValueProvider, ProjectMetaConventions, ResourceFactory.Provider {
42+
companion object {
43+
private const val PLUGIN_CLASS_PATTERN: String = "^(?!io\\.papermc\\.)([a-zA-Z_$][a-zA-Z\\d_$]*\\.)*[a-zA-Z_$][a-zA-Z\\d_$]*$"
44+
}
3945

4046
@get:Input
4147
val apiVersion: Property<String> = objects.property()
4248

49+
@Pattern("^[A-Za-z0-9_\\.-]+$", "Paper plugin name")
4350
@get:Input
4451
val name: Property<String> = objects.property()
4552

4653
@get:Input
4754
val version: Property<String> = objects.property()
4855

56+
@Pattern(PLUGIN_CLASS_PATTERN, "Paper plugin main class name")
4957
@get:Input
5058
val main: Property<String> = objects.property()
5159

60+
@Pattern(PLUGIN_CLASS_PATTERN, "Paper plugin loader class name")
5261
@get:Input
5362
@get:Optional
5463
val loader: Property<String> = objects.property()
5564

65+
@Pattern(PLUGIN_CLASS_PATTERN, "Paper plugin bootstrapper class name")
5666
@get:Input
5767
@get:Optional
5868
val bootstrapper: Property<String> = objects.property()
@@ -178,11 +188,11 @@ class PaperPluginYml constructor(
178188
@ConfigSerializable
179189
class Serializable(yml: PaperPluginYml) {
180190
val apiVersion = yml.apiVersion.get()
181-
val name = yml.name.get()
191+
val name = yml::name.getValidating()
182192
val version = yml.version.get()
183-
val main = yml.main.get()
184-
val loader = yml.loader.orNull
185-
val bootstrapper = yml.bootstrapper.orNull
193+
val main = yml::main.getValidating()
194+
val loader = yml::loader.orNullValidating()
195+
val bootstrapper = yml::bootstrapper.orNullValidating()
186196
val description = yml.description.orNull
187197
val author = yml.author.orNull
188198
val authors = yml.authors.nullIfEmpty()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package xyz.jpenilla.resourcefactory.util
2+
3+
import org.intellij.lang.annotations.Language
4+
5+
@Target(AnnotationTarget.PROPERTY)
6+
annotation class Pattern(
7+
@Language("RegExp")
8+
val pattern: String,
9+
val description: String = ""
10+
)

plugin/src/main/kotlin/xyz/jpenilla/resourcefactory/util/ext.kt

+40
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
package xyz.jpenilla.resourcefactory.util
22

3+
import org.gradle.api.GradleException
34
import org.gradle.api.NamedDomainObjectContainer
45
import org.gradle.api.provider.ListProperty
56
import org.gradle.api.provider.MapProperty
7+
import org.gradle.api.provider.Property
68
import org.gradle.api.provider.SetProperty
9+
import org.intellij.lang.annotations.Language
10+
import java.util.regex.Pattern
11+
import kotlin.reflect.KProperty
12+
import kotlin.reflect.full.findAnnotation
13+
import kotlin.reflect.jvm.javaField
14+
import kotlin.reflect.jvm.javaGetter
715

816
fun <T> ListProperty<T>.nullIfEmpty(): List<T>? = if (get().isEmpty()) null else get().toList()
917

@@ -12,3 +20,35 @@ fun <T> SetProperty<T>.nullIfEmpty(): Set<T>? = if (get().isEmpty()) null else g
1220
fun <A, B> MapProperty<A, B>.nullIfEmpty(): Map<A, B>? = if (get().isEmpty()) null else get().toMap()
1321

1422
fun <A> NamedDomainObjectContainer<A>.nullIfEmpty(): Map<String, A>? = if (isEmpty()) null else asMap.toMap()
23+
24+
fun KProperty<String>.validate(): String =
25+
orNullValidating { it } ?: throw NullPointerException()
26+
27+
fun KProperty<Property<String>>.getValidating(): String =
28+
orNullValidating { it.get() } ?: throw NullPointerException()
29+
30+
fun KProperty<Property<String>>.orNullValidating(): String? = orNullValidating { it.orNull }
31+
32+
fun <T : Any?> KProperty<T>.orNullValidating(
33+
stringGetter: (T) -> String?,
34+
): String? {
35+
val value = stringGetter(getter.call())
36+
val declrCls = javaField?.declaringClass?.simpleName
37+
?: javaGetter?.declaringClass?.simpleName
38+
?: throw IllegalArgumentException("Cannot find owning class for property $this")
39+
val fallbackDesc = "$declrCls.$name"
40+
val annotation = findAnnotation<xyz.jpenilla.resourcefactory.util.Pattern>()
41+
?: throw GradleException("Property $fallbackDesc is not annotated with @Pattern.")
42+
return value?.validate(
43+
annotation.pattern,
44+
annotation.description.takeIf { it.isNotBlank() } ?: fallbackDesc
45+
)
46+
}
47+
48+
fun String.validate(@Language("RegExp") pattern: String, description: String): String {
49+
val regex = Pattern.compile(pattern)
50+
if (!regex.matcher(this).matches()) {
51+
throw GradleException("Invalid $description '$this', must match pattern '$pattern'.")
52+
}
53+
return this
54+
}

plugin/src/main/kotlin/xyz/jpenilla/resourcefactory/velocity/VelocityPluginJson.kt

+11-2
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ import org.gradle.kotlin.dsl.property
1414
import org.spongepowered.configurate.objectmapping.ConfigSerializable
1515
import xyz.jpenilla.resourcefactory.ConfigurateSingleFileResourceFactory
1616
import xyz.jpenilla.resourcefactory.ResourceFactory
17+
import xyz.jpenilla.resourcefactory.util.Pattern
1718
import xyz.jpenilla.resourcefactory.util.ProjectMetaConventions
19+
import xyz.jpenilla.resourcefactory.util.getValidating
1820
import xyz.jpenilla.resourcefactory.util.nullAction
1921
import xyz.jpenilla.resourcefactory.util.nullIfEmpty
22+
import xyz.jpenilla.resourcefactory.util.validate
2023

2124
fun Project.velocityPluginJson(configure: Action<VelocityPluginJson> = nullAction()): VelocityPluginJson {
2225
val yml = VelocityPluginJson(objects)
@@ -30,6 +33,11 @@ class VelocityPluginJson constructor(
3033
private val objects: ObjectFactory
3134
) : ConfigurateSingleFileResourceFactory.ObjectMapper.ValueProvider, ProjectMetaConventions, ResourceFactory.Provider {
3235

36+
companion object {
37+
private const val PLUGIN_ID_PATTERN: String = "[a-z][a-z0-9-_]{0,63}"
38+
}
39+
40+
@Pattern(PLUGIN_ID_PATTERN, "Velocity plugin id")
3341
@get:Input
3442
val id: Property<String> = objects.property()
3543

@@ -81,6 +89,7 @@ class VelocityPluginJson constructor(
8189

8290
@ConfigSerializable
8391
class Dependency(
92+
@Pattern(PLUGIN_ID_PATTERN, "Velocity plugin id (of dependency)")
8493
@get:Input
8594
val id: String,
8695
@get:Input
@@ -89,12 +98,12 @@ class VelocityPluginJson constructor(
8998

9099
@ConfigSerializable
91100
class Serializable(json: VelocityPluginJson) {
92-
val id = json.id.get()
101+
val id = json::id.getValidating()
93102
val name = json.name.orNull
94103
val version = json.version.orNull
95104
val description = json.description.orNull
96105
val url = json.url.orNull
97-
val dependencies = json.dependencies.nullIfEmpty()
106+
val dependencies = json.dependencies.nullIfEmpty()?.onEach { it::id.validate() }
98107
val main = json.main.get()
99108
}
100109
}

0 commit comments

Comments
 (0)