Skip to content

fix: allow to deserialize multi-resource json documents (#855) #863

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
******************************************************************************/
package com.redhat.devtools.intellij.kubernetes.editor

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ArrayNode
import com.intellij.json.JsonFileType
import com.intellij.openapi.fileTypes.FileType
import com.redhat.devtools.intellij.kubernetes.model.util.ResourceException
Expand All @@ -22,7 +24,6 @@ import org.jetbrains.yaml.YAMLFileType
object EditorResourceSerialization {

const val RESOURCE_SEPARATOR_YAML = "\n---"
private const val RESOURCE_SEPARATOR_JSON = ",\n"

/**
* Returns a list of [HasMetadata] for a given yaml or json string.
Expand All @@ -41,60 +42,99 @@ object EditorResourceSerialization {
* @see JsonFileType.INSTANCE
*/
fun deserialize(jsonYaml: String?, fileType: FileType?, currentNamespace: String?): List<HasMetadata> {
return if (jsonYaml == null
|| !isSupported(fileType)) {
emptyList()
} else {
val resources = jsonYaml
.split(RESOURCE_SEPARATOR_YAML)
.filter { jsonYaml -> jsonYaml.isNotBlank() }
if (resources.size > 1
&& YAMLFileType.YML != fileType) {
throw ResourceException(
"${fileType?.name ?: "File type"} is not supported for multi-resource documents. Only ${YAMLFileType.YML.name} is.")
return try {
when {
jsonYaml == null ->
emptyList()

YAMLFileType.YML == fileType ->
yaml2Resources(jsonYaml, currentNamespace)

JsonFileType.INSTANCE == fileType ->
json2Resources(jsonYaml, currentNamespace)

else ->
emptyList()
}
try {
resources
.map { jsonYaml ->
setMissingNamespace(currentNamespace, createResource<GenericKubernetesResource>(jsonYaml))
} catch (e: RuntimeException) {
throw ResourceException("Invalid kubernetes yaml/json", e.cause ?: e)
}
}

private fun yaml2Resources(yaml: String, currentNamespace: String?): List<HasMetadata> {
val resources = yaml
.split(RESOURCE_SEPARATOR_YAML)
.filter { yaml ->
yaml.isNotBlank()
}
return resources
.map { yaml ->
setMissingNamespace(currentNamespace, createResource<GenericKubernetesResource>(yaml))
}
.toList()
}

private fun json2Resources(json: String?, currentNamespace: String?): List<HasMetadata> {
val mapper = ObjectMapper()
val rootNode = mapper.readTree(json)
return when {
rootNode.isArray ->
(rootNode as ArrayNode)
.mapNotNull { node ->
setMissingNamespace(currentNamespace, mapper.treeToValue(node, GenericKubernetesResource::class.java))
}
.toList()
} catch (e: RuntimeException) {
throw ResourceException("Invalid kubernetes yaml/json", e.cause ?: e)
}
rootNode.isObject ->
listOf(
setMissingNamespace(currentNamespace,
mapper.treeToValue(rootNode, GenericKubernetesResource::class.java)
)
)
else ->
emptyList()
}
}

private fun setMissingNamespace(namespace: String?, resource: HasMetadata): HasMetadata {
if (resource.metadata.namespace.isNullOrEmpty()
if (resource.metadata != null
&& resource.metadata.namespace.isNullOrEmpty()
&& namespace != null) {
resource.metadata.namespace = namespace
}
return resource
}

fun serialize(resources: List<HasMetadata>, fileType: FileType?): String? {
if (fileType == null) {
return null
}
if (resources.size >= 2
&& fileType != YAMLFileType.YML) {
throw UnsupportedOperationException(
"${fileType.name} is not supported for multi-resource documents. Only ${YAMLFileType.YML.name} is.")
return try {
when {
fileType == null ->
null

YAMLFileType.YML == fileType ->
resources2yaml(resources)

JsonFileType.INSTANCE == fileType ->
resources2json(resources)

else ->
""
}
} catch (e: RuntimeException) {
throw ResourceException("Invalid kubernetes yaml/json", e.cause ?: e)
}
return resources
.mapNotNull { resource -> serialize(resource, fileType) }
.joinToString("\n")
}

private fun resources2yaml(resources: List<HasMetadata>): String {
return resources.joinToString("\n") { resource ->
Serialization.asYaml(resource).trim()
}
}

private fun serialize(resource: HasMetadata, fileType: FileType): String? {
return when(fileType) {
YAMLFileType.YML ->
Serialization.asYaml(resource).trim()
JsonFileType.INSTANCE ->
Serialization.asJson(resource).trim()
else -> null
private fun resources2json(resources: List<HasMetadata>): String {
return if (resources.size == 1) {
Serialization.asJson(resources.first()).trim()
} else {
Serialization.asJson(resources).trim()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import com.intellij.json.JsonFileType
import com.intellij.openapi.fileTypes.PlainTextFileType
import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.resource
import com.redhat.devtools.intellij.kubernetes.model.util.ResourceException
import io.fabric8.kubernetes.api.model.HasMetadata
import io.fabric8.kubernetes.api.model.Pod
import io.fabric8.kubernetes.client.utils.Serialization
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.assertj.core.api.Assertions.tuple
import org.jetbrains.yaml.YAMLFileType
import org.junit.Test

Expand Down Expand Up @@ -57,19 +59,20 @@ class EditorResourceSerializationTest {
}

@Test
fun `#deserialize throws if given multiple json resources`() {
fun `#deserialize returns list of resources if given multiple JSON resources`() {
// given
val json = """
{"apiVersion": "v1", "kind": "Pod"}
---
{"apiVersion": "v1", "kind": "Service"}
[
{"apiVersion": "v1", "kind": "Pod"},
{"apiVersion": "v1", "kind": "Service"}
]
""".trimIndent()

assertThatThrownBy {
// when
EditorResourceSerialization.deserialize(json, JsonFileType.INSTANCE, null)
// then
}.isInstanceOf(ResourceException::class.java)
val deserialized = EditorResourceSerialization.deserialize(json, JsonFileType.INSTANCE, null)
assertThat(deserialized)
.extracting(HasMetadata::getKind, HasMetadata::getApiVersion)
.containsExactlyInAnyOrder(
tuple("Pod", "v1"),
tuple("Service", "v1"))
}

@Test
Expand Down Expand Up @@ -203,17 +206,17 @@ class EditorResourceSerializationTest {
}

@Test
fun `#serialize throws if given multiple resources and non-YAML file type`() {
fun `#serialize returns json array for given multiple resources and JSON file type`() {
// given
val resources = listOf(
resource<Pod>("darth vader"),
resource<Pod>("emperor")
)
assertThatThrownBy {
// when
EditorResourceSerialization.serialize(resources, JsonFileType.INSTANCE)
// then
}.isInstanceOf(UnsupportedOperationException::class.java)
val expected = Serialization.asJson(resources).trim()
// when
val serialized = EditorResourceSerialization.serialize(resources, JsonFileType.INSTANCE)
// then
assertThat(serialized).isEqualTo(expected)
}

@Test
Expand Down Expand Up @@ -259,7 +262,7 @@ class EditorResourceSerializationTest {
}

@Test
fun `#serialize returns null if given unsupported file type`() {
fun `#serialize returns '' if given unsupported file type`() {
// given
val resource = resource<Pod>("leia")
// when
Expand All @@ -268,5 +271,4 @@ class EditorResourceSerializationTest {
assertThat(serialized)
.isEqualTo("")
}

}
Loading