Skip to content

Commit

Permalink
feat: aggregate downloaded & extracted JSON schemas in a single file
Browse files Browse the repository at this point in the history
Closes #49
  • Loading branch information
fstaudt committed Apr 2, 2023
1 parent 0056747 commit f795ee3
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ class JsonSchemaAggregator(
jsonSchema.put("\$id", "${chart.name}/${chart.version}/${AGGREGATED_SCHEMA_FILE}")
jsonSchema.put("title", "Configuration for chart ${chart.name}:${chart.version}")
jsonSchema.updateReferencesFor(chart.dependencies.toDownloadedRefMappings())
jsonSchema.aggregateDownloadedSchemasFor(chart)
jsonSchema.updateReferencesFor(chart.dependencies.toLocallyStoredRefMappings())
jsonSchema.removeGeneratedGlobalDescription()
jsonSchema.setExtractedDependencyReferencesFrom(extractSchemasDir, extractSchemasDir.name)
jsonSchema.removeGeneratedGlobalPropertiesDescription()
jsonSchema.setExtractedDependencyReferencesFrom(extractSchemasDir, "#/refs/${extractSchemasDir.name}")
jsonSchema.addGlobalPropertiesDescriptionFor(chart)
chartSchema.takeIf { it.exists() }?.let { jsonSchema.put("\$ref", schemaLocator.schemaFor(chartDir)) }
return aggregatedJsonPatch?.apply(jsonSchema) ?: jsonSchema
Expand All @@ -62,7 +63,7 @@ class JsonSchemaAggregator(
}
}

private fun ObjectNode.removeGeneratedGlobalDescription() {
private fun ObjectNode.removeGeneratedGlobalPropertiesDescription() {
properties().global().allOf().also { allOf ->
allOf.removeAll { it.get("title")?.textValue()?.startsWith(GLOBAL_VALUES_TITLE) ?: false }
}
Expand All @@ -77,7 +78,7 @@ class JsonSchemaAggregator(
}
setExtractedDependencyReferencesFrom(it, "$refPrefix/${it.name}")
properties().global().put("additionalProperties", false)
addGlobalPropertiesDescriptionFor("$refPrefix/${it.name}".removePrefix("${extractSchemasDir.name}/"))
addGlobalPropertiesDescriptionFor("$refPrefix/${it.name}".removePrefix("#/refs/${extractSchemasDir.name}/"))
}
addGlobalPropertiesFrom(it, refPrefix)
}
Expand All @@ -86,7 +87,7 @@ class JsonSchemaAggregator(

private fun ObjectNode.addGlobalPropertiesFrom(schemasDir: File, refPrefix: String) {
if (schemasDir.containsFile(HELM_SCHEMA_FILE)) {
val ref = "$refPrefix/${schemasDir.name}/$HELM_SCHEMA_FILE#/properties/global"
val ref = "$refPrefix/${schemasDir.name}/$HELM_SCHEMA_FILE/properties/global"
global().allOf().add(ObjectNode(nodeFactory).put("\$ref", ref))
}
if (schemasDir.hasSubDirectories()) {
Expand Down Expand Up @@ -121,6 +122,23 @@ class JsonSchemaAggregator(
)
}

private fun ObjectNode.aggregateDownloadedSchemasFor(chart: Chart) {
chart.dependencies.filter { repositoryMappings.contains(it.repository) }.forEach {
val mapping = repositoryMappings[it.repository]!!
aggregateDownloadedSchemaFor(URI("${it.fullUri()}/${mapping.valuesSchemaFile}"))
aggregateDownloadedSchemaFor(URI("${it.fullUri()}/${mapping.globalValuesSchemaFile}"))
}
}

private fun ObjectNode.aggregateDownloadedSchemaFor(schemaUri: URI) {
val schemaNode = schemaUri.path.split("/").filter { it.isNotBlank() }
.fold(objectNode("refs").objectNode(downloadSchemasDir.name)) { node, s -> node.objectNode(s) }
val schema = runCatching {
jsonMapper.readTree(File(downloadSchemasDir, schemaUri.path))
}.getOrDefault(nodeFactory.objectNode())
schemaNode.setAll<JsonNode>(schema as ObjectNode)
}

private fun ChartDependency.fullUri() = repositoryMappings[repository]?.let { "${it.baseUri}/$name/$version" }

private fun ChartDependency.fullName(): String {
Expand All @@ -136,15 +154,21 @@ class JsonSchemaAggregator(

private data class RefMapping(val baseUri: String, val mappedBaseUri: String) {
fun matches(ref: JsonNode) = ref.textValue().startsWith(baseUri)
fun map(ref: JsonNode) = TextNode(ref.textValue().replace(baseUri, mappedBaseUri))
fun map(ref: JsonNode): JsonNode {
return if (mappedBaseUri.startsWith("#")) {
TextNode(ref.textValue().replace("#", "").replace(baseUri, mappedBaseUri))
} else {
TextNode(ref.textValue().replace(baseUri, mappedBaseUri))
}
}
}

private fun List<ChartDependency>.toDownloadedRefMappings() = mapNotNull { it.toDownloadedRefMapping() }
private fun ChartDependency.toDownloadedRefMapping(): RefMapping? {
return repositoryMappings[repository]?.let {
RefMapping(
"${it.baseUri}/$name/$version",
"${downloadSchemasDir.name}${URI(it.baseUri).path}/$name/$version"
"#/refs/${downloadSchemasDir.name}${URI(it.baseUri).path}/$name/$version"
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.github.fstaudt.helm.JsonSchemaGenerator.Companion.GLOBAL_VALUES_TITLE
import io.github.fstaudt.helm.model.Chart
import io.github.fstaudt.helm.model.ChartDependency
import io.github.fstaudt.helm.model.JsonSchemaRepository
import io.github.fstaudt.helm.test.assertions.escaped
import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
Expand All @@ -30,8 +31,11 @@ internal class JsonSchemaAggregatorTest {
private const val THIRDPARTY = "@thirdparty"
private const val EXTERNAL_SCHEMA = "external-json-schema"
private const val EXTERNAL_VERSION = "0.2.0"
private const val EXTERNAL_SCHEMA_PATH = "$EXTERNAL_SCHEMA/$EXTERNAL_VERSION/$VALUES_SCHEMA_FILE"
private const val EXTERNAL_GLOBAL_SCHEMA_PATH = "$EXTERNAL_SCHEMA/$EXTERNAL_VERSION/$GLOBAL_VALUES_SCHEMA_FILE"
private const val EXTERNAL_SCHEMAS_PATH = "$EXTERNAL_SCHEMA/$EXTERNAL_VERSION"
private const val EXTERNAL_VALUES_SCHEMA_PATH = "$EXTERNAL_SCHEMAS_PATH/$VALUES_SCHEMA_FILE"
private const val EXTERNAL_GLOBAL_SCHEMA_PATH = "$EXTERNAL_SCHEMAS_PATH/$GLOBAL_VALUES_SCHEMA_FILE"
private const val VALUES_SCHEMA = "values.json"
private const val GLOBAL_SCHEMA = "global.json"
private const val EMBEDDED_SCHEMA = "embedded-json-schema"
private const val EMBEDDED_VERSION = "0.1.0"
private const val EMBEDDED_SUB_SCHEMA = "embedded-sub-json-schema"
Expand Down Expand Up @@ -86,16 +90,54 @@ internal class JsonSchemaAggregatorTest {
ChartDependency(EXTERNAL_SCHEMA, EXTERNAL_VERSION, APPS),
ChartDependency(NO_SCHEMA, EMBEDDED_VERSION, THIRDPARTY),
))
testProject.initDownloadedSchemas("$APPS_PATH/$EXTERNAL_SCHEMAS_PATH")
val json = aggregator.aggregate(chart, null, null)
assertThatJson(json).node("properties").and({
it.node("global.allOf").isArray.hasSize(3)
it.node("global.allOf[0].\$ref")
.isEqualTo("$DOWNLOADS_DIR/$APPS_PATH/$EXTERNAL_SCHEMA_PATH#/properties/global")
it.node("global.allOf[1].\$ref").isEqualTo("$DOWNLOADS_DIR/$APPS_PATH/$EXTERNAL_GLOBAL_SCHEMA_PATH")
.isEqualTo("#/refs/$DOWNLOADS_DIR/$APPS_PATH/$EXTERNAL_VALUES_SCHEMA_PATH/properties/global")
it.node("global.allOf[1].\$ref").isEqualTo("#/refs/$DOWNLOADS_DIR/$APPS_PATH/$EXTERNAL_GLOBAL_SCHEMA_PATH")
it.node("global.allOf[2].title").isString.startsWith(GLOBAL_VALUES_TITLE)
it.node("$EXTERNAL_SCHEMA.\$ref").isEqualTo("$DOWNLOADS_DIR/$APPS_PATH/$EXTERNAL_SCHEMA_PATH")
it.node("$EXTERNAL_SCHEMA.\$ref").isEqualTo("#/refs/$DOWNLOADS_DIR/$APPS_PATH/$EXTERNAL_VALUES_SCHEMA_PATH")
it.isObject.doesNotContainKey(NO_SCHEMA)
})
assertThatJson(json).node("refs.$DOWNLOADS_DIR.$APPS_PATH.$EXTERNAL_SCHEMA.${EXTERNAL_VERSION.escaped()}").and({
it.node("${VALUES_SCHEMA_FILE.escaped()}.\$id").isEqualTo("$APPS_PATH/$EXTERNAL_VALUES_SCHEMA_PATH")
it.node("${GLOBAL_VALUES_SCHEMA_FILE.escaped()}.\$id").isEqualTo("$APPS_PATH/$EXTERNAL_GLOBAL_SCHEMA_PATH")
})
}

@Test
fun `aggregate should aggregate downloaded JSON schemas for dependencies in repository with custom schema names`() {
val chart = Chart("v2", CHART_NAME, CHART_VERSION, listOf(
ChartDependency(EXTERNAL_SCHEMA, EXTERNAL_VERSION, APPS),
))
testProject.initDownloadedSchemas("$APPS_PATH/$EXTERNAL_SCHEMAS_PATH", VALUES_SCHEMA, GLOBAL_SCHEMA)
val customRepositoryMappings = mapOf(
APPS to JsonSchemaRepository("$REPOSITORY_URL/$APPS_PATH", null, null, VALUES_SCHEMA, GLOBAL_SCHEMA)
)
val aggregator = JsonSchemaAggregator(
customRepositoryMappings,
TestSchemaLocator(),
testProject,
testProject.downloadSchemasDir,
testProject.extractSchemasDir)
val json = aggregator.aggregate(chart, null, null)
assertThatJson(json).node("properties").and({
it.node("global.allOf").isArray.hasSize(3)
it.node("global.allOf[0].\$ref")
.isEqualTo("#/refs/$DOWNLOADS_DIR/$APPS_PATH/$EXTERNAL_SCHEMAS_PATH/$VALUES_SCHEMA/properties/global")
it.node("global.allOf[1].\$ref")
.isEqualTo("#/refs/$DOWNLOADS_DIR/$APPS_PATH/$EXTERNAL_SCHEMAS_PATH/$GLOBAL_SCHEMA")
it.node("global.allOf[2].title").isString.startsWith(GLOBAL_VALUES_TITLE)
it.node("$EXTERNAL_SCHEMA.\$ref")
.isEqualTo("#/refs/$DOWNLOADS_DIR/$APPS_PATH/$EXTERNAL_SCHEMAS_PATH/$VALUES_SCHEMA")
it.isObject.doesNotContainKey(NO_SCHEMA)
})
assertThatJson(json).node("refs.$DOWNLOADS_DIR.$APPS_PATH.$EXTERNAL_SCHEMA.${EXTERNAL_VERSION.escaped()}").and({
it.node("${VALUES_SCHEMA.escaped()}.\$id").isEqualTo("$APPS_PATH/$EXTERNAL_SCHEMAS_PATH/$VALUES_SCHEMA")
it.node("${GLOBAL_SCHEMA.escaped()}.\$id").isEqualTo("$APPS_PATH/$EXTERNAL_SCHEMAS_PATH/$GLOBAL_SCHEMA")
})
}

@Test
Expand Down Expand Up @@ -225,9 +267,10 @@ internal class JsonSchemaAggregatorTest {
val json = aggregator.aggregate(chart, null, null)
assertThatJson(json).node("properties").and({
it.node("global.allOf[0].\$ref")
.isEqualTo("$DOWNLOADS_DIR/$APPS_PATH/$EXTERNAL_SCHEMA_PATH#/properties/global")
it.node("global.allOf[1].\$ref").isEqualTo("$DOWNLOADS_DIR/$APPS_PATH/$EXTERNAL_GLOBAL_SCHEMA_PATH")
it.node("$EXTERNAL_SCHEMA-alias.\$ref").isEqualTo("$DOWNLOADS_DIR/$APPS_PATH/$EXTERNAL_SCHEMA_PATH")
.isEqualTo("#/refs/$DOWNLOADS_DIR/$APPS_PATH/$EXTERNAL_VALUES_SCHEMA_PATH/properties/global")
it.node("global.allOf[1].\$ref").isEqualTo("#/refs/$DOWNLOADS_DIR/$APPS_PATH/$EXTERNAL_GLOBAL_SCHEMA_PATH")
it.node("$EXTERNAL_SCHEMA-alias.\$ref")
.isEqualTo("#/refs/$DOWNLOADS_DIR/$APPS_PATH/$EXTERNAL_VALUES_SCHEMA_PATH")
})
}

Expand All @@ -239,9 +282,9 @@ internal class JsonSchemaAggregatorTest {
testProject.initExtractedSchemas(EMBEDDED_SCHEMA)
val json = aggregator.aggregate(chart, null, null)
assertThatJson(json).node("properties").and({
val extractedSchemaFile = "$EXTRACT_DIR/$EMBEDDED_SCHEMA/$HELM_SCHEMA_FILE"
val extractedSchemaFile = "#/refs/$EXTRACT_DIR/$EMBEDDED_SCHEMA/$HELM_SCHEMA_FILE"
it.node("$EMBEDDED_SCHEMA.\$ref").isEqualTo(extractedSchemaFile)
it.node("global.allOf[0].\$ref").isEqualTo("$extractedSchemaFile#/properties/global")
it.node("global.allOf[0].\$ref").isEqualTo("$extractedSchemaFile/properties/global")
})
}

Expand All @@ -254,15 +297,16 @@ internal class JsonSchemaAggregatorTest {
val json = aggregator.aggregate(chart, null, null)
assertThatJson(json).node("properties").and(
{
val extractedSubSchemaFile = "$EXTRACT_DIR/$EMBEDDED_SUB_SCHEMA/$EMBEDDED_SCHEMA/$HELM_SCHEMA_FILE"
val extractedSubSchemaFile =
"#/refs/$EXTRACT_DIR/$EMBEDDED_SUB_SCHEMA/$EMBEDDED_SCHEMA/$HELM_SCHEMA_FILE"
it.node(EMBEDDED_SUB_SCHEMA).isObject.doesNotContainKey("\$ref")
it.node("$EMBEDDED_SUB_SCHEMA.properties.$EMBEDDED_SCHEMA.\$ref").isEqualTo(extractedSubSchemaFile)
it.node("$EMBEDDED_SUB_SCHEMA.properties.$EMBEDDED_SCHEMA.properties").isObject.containsOnlyKeys("global")
it.node("global.allOf").isArray.hasSize(2)
it.node("global.allOf[0].\$ref").isEqualTo("$extractedSubSchemaFile#/properties/global")
it.node("global.allOf[0].\$ref").isEqualTo("$extractedSubSchemaFile/properties/global")
it.node("$EMBEDDED_SUB_SCHEMA.properties").and({
it.node("global.allOf").isArray.hasSize(2)
it.node("global.allOf[0].\$ref").isEqualTo("$extractedSubSchemaFile#/properties/global")
it.node("global.allOf[0].\$ref").isEqualTo("$extractedSubSchemaFile/properties/global")
it.node("global.allOf[1].title").isString.startsWith(EXTRACTED_GLOBAL_VALUES_TITLE)
})
it.node("global.allOf[1].title").isString.startsWith(GLOBAL_VALUES_TITLE)
Expand All @@ -280,18 +324,19 @@ internal class JsonSchemaAggregatorTest {
val json = aggregator.aggregate(chart, null, null)
assertThatJson(json).node("properties").and(
{
val extractedSchemaFile = "$EXTRACT_DIR/$EMBEDDED_SUB_SCHEMA/$HELM_SCHEMA_FILE"
val extractedSubSchemaFile = "$EXTRACT_DIR/$EMBEDDED_SUB_SCHEMA/$EMBEDDED_SCHEMA/$HELM_SCHEMA_FILE"
val extractedSchemaFile = "#/refs/$EXTRACT_DIR/$EMBEDDED_SUB_SCHEMA/$HELM_SCHEMA_FILE"
val extractedSubSchemaFile =
"#/refs/$EXTRACT_DIR/$EMBEDDED_SUB_SCHEMA/$EMBEDDED_SCHEMA/$HELM_SCHEMA_FILE"
it.node("$EMBEDDED_SUB_SCHEMA.\$ref").isEqualTo(extractedSchemaFile)
it.node("$EMBEDDED_SUB_SCHEMA.properties").isObject.doesNotContainKey(HELM_SCHEMA_FILE)
it.node("$EMBEDDED_SUB_SCHEMA.properties.$EMBEDDED_SCHEMA.\$ref").isEqualTo(extractedSubSchemaFile)
it.node("$EMBEDDED_SUB_SCHEMA.properties.$EMBEDDED_SCHEMA.properties").isObject.containsOnlyKeys("global")
it.node("global.allOf").isArray.hasSize(3)
it.node("global.allOf[0].\$ref").isEqualTo("$extractedSchemaFile#/properties/global")
it.node("global.allOf[1].\$ref").isEqualTo("$extractedSubSchemaFile#/properties/global")
it.node("global.allOf[0].\$ref").isEqualTo("$extractedSchemaFile/properties/global")
it.node("global.allOf[1].\$ref").isEqualTo("$extractedSubSchemaFile/properties/global")
it.node("$EMBEDDED_SUB_SCHEMA.properties").and({
it.node("global.allOf").isArray.hasSize(2)
it.node("global.allOf[0].\$ref").isEqualTo("$extractedSubSchemaFile#/properties/global")
it.node("global.allOf[0].\$ref").isEqualTo("$extractedSubSchemaFile/properties/global")
it.node("global.allOf[1].title")
.isEqualTo("$EXTRACTED_GLOBAL_VALUES_TITLE $EMBEDDED_SUB_SCHEMA dependency")
it.node("global.allOf[1].description").isEqualTo("\\n\\\\n")
Expand All @@ -315,7 +360,7 @@ internal class JsonSchemaAggregatorTest {
testProject.initExtractedSchemas("$EMBEDDED_SCHEMA-alias")
val json = aggregator.aggregate(chart, null, null)
assertThatJson(json).node("properties.$EMBEDDED_SCHEMA-alias.\$ref")
.isEqualTo("$EXTRACT_DIR/$EMBEDDED_SCHEMA-alias/$HELM_SCHEMA_FILE")
.isEqualTo("#/refs/$EXTRACT_DIR/$EMBEDDED_SCHEMA-alias/$HELM_SCHEMA_FILE")
}

@Test
Expand All @@ -326,7 +371,7 @@ internal class JsonSchemaAggregatorTest {
testProject.initExtractedSchemas(EMBEDDED_SCHEMA)
val json = aggregator.aggregate(chart, null, null)
assertThatJson(json).node("properties.$EMBEDDED_SCHEMA.\$ref")
.isEqualTo("$EXTRACT_DIR/$EMBEDDED_SCHEMA/$HELM_SCHEMA_FILE")
.isEqualTo("#/refs/$EXTRACT_DIR/$EMBEDDED_SCHEMA/$HELM_SCHEMA_FILE")
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,31 @@ fun testProject(parentFolder: File? = File("build/tmp")): TestProject {
}
}

fun TestProject.initExtractedSchemas(dependencyName: String) {
File(extractSchemasDir, dependencyName).mkdirs()
File(extractSchemasDir, "$dependencyName/$HELM_SCHEMA_FILE").writeText("{}")
fun TestProject.initExtractedSchemas(dependencyPath: String) {
File(extractSchemasDir, dependencyPath).mkdirs()
File(extractSchemasDir, "$dependencyPath/$HELM_SCHEMA_FILE").writeText("""
{
"${'$'}id": "$dependencyPath/$HELM_SCHEMA_FILE"
}
""".trimIndent())
}

fun TestProject.initDownloadedSchemas(
dependencyPath: String,
valuesSchemaFile: String = VALUES_SCHEMA_FILE,
globalSchemaFile: String = GLOBAL_VALUES_SCHEMA_FILE,
) {
File(downloadSchemasDir, dependencyPath).mkdirs()
File(downloadSchemasDir, "$dependencyPath/$valuesSchemaFile").writeText("""
{
"${'$'}id": "$dependencyPath/$valuesSchemaFile"
}
""".trimIndent())
File(downloadSchemasDir, "$dependencyPath/$globalSchemaFile").writeText("""
{
"${'$'}id": "$dependencyPath/$globalSchemaFile"
}
""".trimIndent())
}

fun TestProject.initHelmResources(chartName: String, chartVersion: String) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.github.fstaudt.helm.test.assertions

fun String.escaped() = replace(".", "\\.")

0 comments on commit f795ee3

Please sign in to comment.