Skip to content

Commit c5a443a

Browse files
committed
fix: use snakeyaml-engine-kmp for validation
1 parent 0cb7a86 commit c5a443a

File tree

51 files changed

+668
-94
lines changed

Some content is hidden

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

51 files changed

+668
-94
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ kotlin {
8181
jvmMain {
8282
dependencies {
8383
implementation("com.charleskorn.kaml:kaml:0.72.0")
84+
implementation("it.krzeminski:snakeyaml-engine-kmp:3.1.1")
8485
}
8586
}
8687

src/jvmMain/kotlin/it/krzeminski/githubactionstyping/ManifestsToReport.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package it.krzeminski.githubactionstyping
22

3-
import it.krzeminski.githubactionstyping.parsing.TypesManifest
3+
import it.krzeminski.githubactionstyping.parsing.ValidationException
44
import it.krzeminski.githubactionstyping.parsing.parseManifest
55
import it.krzeminski.githubactionstyping.parsing.parseTypesManifest
66
import it.krzeminski.githubactionstyping.reporting.appendStatus
@@ -25,11 +25,14 @@ fun manifestsToReport(manifestAndPath: Pair<String, Path>?, typesManifest: Strin
2525
})
2626
}
2727

28-
val parsedTypesManifest = if (typesManifest.isNotBlank()) {
28+
val parsedTypesManifest =
2929
parseTypesManifest(typesManifest)
30-
} else {
31-
TypesManifest()
32-
}
30+
.onFailure {
31+
if (it is ValidationException) {
32+
return false to it.message
33+
}
34+
throw it
35+
}.getOrThrow()
3336
val parsedManifest = parseManifest(manifestAndPath.first)
3437

3538
val inputsInTypesManifest = parsedTypesManifest.inputs?.keys ?: emptySet()
Lines changed: 106 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,124 @@
11
package it.krzeminski.githubactionstyping.parsing
22

3-
import com.charleskorn.kaml.AnchorsAndAliases
4-
import com.charleskorn.kaml.EmptyYamlDocumentException
5-
import com.charleskorn.kaml.Yaml
6-
import kotlinx.serialization.SerialName
7-
import kotlinx.serialization.Serializable
8-
import kotlinx.serialization.decodeFromString
9-
10-
@Serializable
3+
import it.krzeminski.snakeyaml.engine.kmp.api.Load
4+
import it.krzeminski.snakeyaml.engine.kmp.api.LoadSettings
5+
import it.krzeminski.snakeyaml.engine.kmp.schema.CoreSchema
6+
117
data class TypesManifest(
128
val inputs: Map<String, ApiItem>? = null,
139
val outputs: Map<String, ApiItem>? = null,
1410
)
1511

16-
@Serializable
1712
data class ApiItem(
1813
val type: String? = null,
1914
val name: String? = null,
20-
@SerialName("allowed-values")
2115
val allowedValues: List<String>? = null,
2216
val separator: String? = null,
23-
@SerialName("named-values")
24-
val namedValues: Map<String, String>? = null,
25-
@SerialName("list-item")
17+
val namedValues: Map<String, Int>? = null,
2618
val listItem: ApiItem? = null,
2719
)
2820

29-
private val myYaml = Yaml(
30-
configuration = Yaml.default.configuration.copy(
31-
strictMode = false,
32-
anchorsAndAliases = AnchorsAndAliases.Permitted(),
33-
)
34-
)
35-
36-
fun parseTypesManifest(manifestString: String): TypesManifest =
21+
fun parseTypesManifest(manifestString: String): Result<TypesManifest> =
3722
runCatching {
38-
myYaml.decodeFromString<TypesManifest>(manifestString)
39-
}.getOrElse {
40-
if (it is EmptyYamlDocumentException) {
41-
return TypesManifest()
23+
val loadedTypesManifest = Load(
24+
// work-around for https://github.com/krzema12/snakeyaml-engine-kmp/pull/390
25+
LoadSettings.builder().setSchema(CoreSchema()).build()
26+
).loadOne(manifestString)
27+
28+
when (loadedTypesManifest) {
29+
null -> TypesManifest()
30+
31+
is Map<*, *> -> {
32+
val excessKeys = loadedTypesManifest.keys - listOf("inputs", "outputs")
33+
if (excessKeys.isNotEmpty()) {
34+
throw ValidationException(excessKeys.joinToString(prefix = "Excess keys: ", postfix = "."))
35+
}
36+
TypesManifest(
37+
inputs = loadedTypesManifest.toInputsOrOutputs("inputs"),
38+
outputs = loadedTypesManifest.toInputsOrOutputs("outputs"),
39+
)
40+
}
41+
42+
else -> throw ValidationException("Types file must be a mapping.")
4243
}
43-
throw it
4444
}
45+
46+
private fun Map<*, *>.toInputsOrOutputs(key: String): Map<String, ApiItem>? =
47+
when (val inputsOrOutputs = this[key]) {
48+
null -> null
49+
50+
is Map<*, *> -> {
51+
inputsOrOutputs.entries.associate { (key, value) -> key as String to value.toApiItem(key) }
52+
}
53+
54+
else -> throw ValidationException("$key must be a mapping.")
55+
}
56+
57+
private fun Any?.toApiItem(key: String): ApiItem =
58+
when (this) {
59+
null -> ApiItem()
60+
61+
is Map<*, *> -> {
62+
val excessKeys = keys - listOf(
63+
"type",
64+
"name",
65+
"allowed-values",
66+
"separator",
67+
"named-values",
68+
"list-item",
69+
)
70+
if (excessKeys.isNotEmpty()) {
71+
throw ValidationException(excessKeys.joinToString(prefix = "Excess keys: ", postfix = "."))
72+
}
73+
74+
ApiItem(
75+
type = get("type")?.let {
76+
if (it !is String) {
77+
throw ValidationException("type must be a string.")
78+
}
79+
it
80+
},
81+
name = get("name")?.let {
82+
if (it !is String) {
83+
throw ValidationException("name must be a string.")
84+
}
85+
it
86+
},
87+
allowedValues = get("allowed-values")?.let {
88+
if (it !is List<*>) {
89+
throw ValidationException("allowed-values must be a sequence.")
90+
}
91+
it.map {
92+
if (it !is String) {
93+
throw ValidationException("Allowed Value must be string.")
94+
}
95+
it
96+
}
97+
},
98+
separator = get("separator")?.let {
99+
if (it !is String) {
100+
throw ValidationException("separator must be string.")
101+
}
102+
it
103+
},
104+
namedValues = get("named-values")?.let {
105+
if (it !is Map<*, *>) {
106+
throw ValidationException("named-values must be a mapping.")
107+
}
108+
it.mapKeys { (key, _) -> key as String }.mapValues { (_, value) ->
109+
if (value !is Int) {
110+
throw ValidationException("Named values must be integer.")
111+
}
112+
value
113+
}
114+
},
115+
listItem = toApiItem("list-item"),
116+
)
117+
}
118+
119+
else -> throw ValidationException("$key must be a mapping.")
120+
}
121+
122+
private fun Map<*, *>.toApiItem(key: String): ApiItem? = get(key)?.toApiItem(key)
123+
124+
internal class ValidationException(override val message: String) : Exception(message)

src/jvmMain/kotlin/it/krzeminski/githubactionstyping/validation/types/Integer.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ fun ApiItem.validateInteger(): ItemValidationResult {
1313
if (this.namedValues?.keys?.any { it.isBlank() } == true) {
1414
return ItemValidationResult.Invalid("Named value names must not be empty.")
1515
}
16-
if (this.namedValues?.values?.any { it.toIntOrNull() == null } == true) {
17-
return ItemValidationResult.Invalid("Named values must be integer.")
18-
}
1916
if (this.name != null && this.name.isBlank()) {
2017
return ItemValidationResult.Invalid("Name must not be empty.")
2118
}

src/jvmTest/kotlin/it/krzeminski/githubactionstyping/LogicConsistencyTest.kt

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.kotest.assertions.withClue
55
import io.kotest.matchers.should
66
import io.kotest.matchers.shouldBe
77
import io.kotest.matchers.types.beOfType
8+
import it.krzeminski.githubactionstyping.parsing.ValidationException
89
import it.krzeminski.githubactionstyping.parsing.parseTypesManifest
910
import it.krzeminski.githubactionstyping.reporting.toPlaintextReport
1011
import it.krzeminski.githubactionstyping.validation.ItemValidationResult
@@ -16,7 +17,7 @@ import java.io.File
1617
*/
1718
class LogicConsistencyTest : UseCaseTest() {
1819
override suspend fun testValid(typing: File) {
19-
val validationResult = parseTypesManifest(typing.readText()).validate(typing.toPath().fileName)
20+
val validationResult = parseTypesManifest(typing.readText()).getOrThrow().validate(typing.toPath().fileName)
2021
withClue(validationResult.toPlaintextReport()) {
2122
validationResult.overallResult shouldBe ItemValidationResult.Valid
2223
}
@@ -29,14 +30,19 @@ class LogicConsistencyTest : UseCaseTest() {
2930
.trimMargin("#")
3031
.trim()
3132

32-
val validationResult = parseTypesManifest(typesManifest).validate(typing.toPath().fileName)
33-
assertSoftly {
34-
validationResult.overallResult should beOfType<ItemValidationResult.Invalid>()
35-
validationResult
36-
.toPlaintextReport()
37-
.trim()
38-
.replace("\u001B", "\\x1B")
39-
.shouldBe(expectedValidationError)
33+
val parsedTypesManifest = parseTypesManifest(typesManifest)
34+
if (parsedTypesManifest.isFailure && parsedTypesManifest.exceptionOrNull() is ValidationException) {
35+
parsedTypesManifest.exceptionOrNull()!!.message shouldBe expectedValidationError
36+
} else {
37+
val validationResult = parsedTypesManifest.getOrThrow().validate(typing.toPath().fileName)
38+
assertSoftly {
39+
validationResult.overallResult should beOfType<ItemValidationResult.Invalid>()
40+
validationResult
41+
.toPlaintextReport()
42+
.trim()
43+
.replace("\u001B", "\\x1B")
44+
.shouldBe(expectedValidationError)
45+
}
4046
}
4147
}
4248
}

src/jvmTest/kotlin/it/krzeminski/githubactionstyping/validation/ManifestValidationTest.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ class ManifestValidationTest : FunSpec({
1717
"integer-input" to ApiItem(type = "integer"),
1818
"integer-with-named-values-input" to ApiItem(
1919
type = "integer",
20-
namedValues = mapOf("foo" to "1", "bar" to "2")
20+
namedValues = mapOf("foo" to 1, "bar" to 2)
2121
),
2222
"integer-with-named-values-and-custom-item-name-input" to ApiItem(
2323
type = "integer",
2424
name = "SomeItemName",
25-
namedValues = mapOf("foo" to "1", "bar" to "2")
25+
namedValues = mapOf("foo" to 1, "bar" to 2)
2626
),
2727
"float-input" to ApiItem(type = "float"),
2828
),
@@ -481,11 +481,11 @@ class ManifestValidationTest : FunSpec({
481481
// given
482482
val manifest = TypesManifest(
483483
inputs = mapOf(
484-
"string-input" to ApiItem(type = "string", namedValues = mapOf("foo" to "1")),
485-
"boolean-input" to ApiItem(type = "boolean", namedValues = mapOf("foo" to "1")),
486-
"float-input" to ApiItem(type = "float", namedValues = mapOf("foo" to "1")),
487-
"list-input" to ApiItem(type = "list", separator = ",", listItem = ApiItem(type = "string"), namedValues = mapOf("foo" to "1")),
488-
"enum-input" to ApiItem(type = "enum", allowedValues = listOf("foo", "bar"), namedValues = mapOf("foo" to "1")),
484+
"string-input" to ApiItem(type = "string", namedValues = mapOf("foo" to 1)),
485+
"boolean-input" to ApiItem(type = "boolean", namedValues = mapOf("foo" to 1)),
486+
"float-input" to ApiItem(type = "float", namedValues = mapOf("foo" to 1)),
487+
"list-input" to ApiItem(type = "list", separator = ",", listItem = ApiItem(type = "string"), namedValues = mapOf("foo" to 1)),
488+
"enum-input" to ApiItem(type = "enum", allowedValues = listOf("foo", "bar"), namedValues = mapOf("foo" to 1)),
489489
),
490490
)
491491

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# yaml-language-server: $schema=../../../../github-actions-typing.schema.json
2+
# See https://github.com/typesafegithub/github-actions-typing
3+
inputs:
4+
foo:
5+
type: string
6+
bar:
7+
baz:
8+
9+
# Expected validation error
10+
#
11+
#Excess keys: bar, baz.
12+
#
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# yaml-language-server: $schema=../../../../github-actions-typing.schema.json
2+
# See https://github.com/typesafegithub/github-actions-typing
3+
outputs:
4+
foo:
5+
type: string
6+
bar:
7+
baz:
8+
9+
# Expected validation error
10+
#
11+
#Excess keys: bar, baz.
12+
#
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# yaml-language-server: $schema=../../../../github-actions-typing.schema.json
2+
# See https://github.com/typesafegithub/github-actions-typing
3+
foo:
4+
bar:
5+
6+
# Expected validation error
7+
#
8+
#Excess keys: foo, bar.
9+
#
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# yaml-language-server: $schema=../../../../github-actions-typing.schema.json
2+
# See https://github.com/typesafegithub/github-actions-typing
3+
inputs:
4+
granted-scopes:
5+
type: list
6+
separator: ','
7+
list-item:
8+
type: enum
9+
allowed-values: foo
10+
11+
# Expected validation error
12+
#
13+
#allowed-values must be a sequence.
14+
#
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# yaml-language-server: $schema=../../../../github-actions-typing.schema.json
2+
# See https://github.com/typesafegithub/github-actions-typing
3+
inputs:
4+
granted-scopes:
5+
type: list
6+
separator: ','
7+
list-item:
8+
type: enum
9+
allowed-values:
10+
- 0x0
11+
12+
# Expected validation error
13+
#
14+
#Allowed Value must be string.
15+
#
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# yaml-language-server: $schema=../../../../github-actions-typing.schema.json
2+
# See https://github.com/typesafegithub/github-actions-typing
3+
inputs:
4+
granted-scopes:
5+
type: list
6+
separator: ','
7+
list-item:
8+
type: enum
9+
name: 0x0
10+
allowed-values:
11+
- read
12+
- write
13+
14+
# Expected validation error
15+
#
16+
#name must be a string.
17+
#
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# yaml-language-server: $schema=../../../../github-actions-typing.schema.json
2+
# See https://github.com/typesafegithub/github-actions-typing
3+
inputs:
4+
permissions:
5+
type: enum
6+
allowed-values: foo
7+
8+
# Expected validation error
9+
#
10+
#allowed-values must be a sequence.
11+
#
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# yaml-language-server: $schema=../../../../github-actions-typing.schema.json
2+
# See https://github.com/typesafegithub/github-actions-typing
3+
inputs:
4+
permissions:
5+
type: enum
6+
allowed-values:
7+
- 0o0
8+
9+
# Expected validation error
10+
#
11+
#Allowed Value must be string.
12+
#
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# yaml-language-server: $schema=../../../../github-actions-typing.schema.json
2+
# See https://github.com/typesafegithub/github-actions-typing
3+
inputs:
4+
permissions:
5+
type: enum
6+
name: 0o0
7+
allowed-values:
8+
- user
9+
- admin
10+
- guest
11+
12+
# Expected validation error
13+
#
14+
#name must be a string.
15+
#

0 commit comments

Comments
 (0)