From 761f59dee291403933618e7bedd3fbd319467cb0 Mon Sep 17 00:00:00 2001 From: Andre Dietisheim Date: Wed, 9 Apr 2025 22:16:03 +0200 Subject: [PATCH] fix: allow to deserialize and serialize multi-resource json docs (#855) Signed-off-by: Andre Dietisheim --- .../editor/EditorResourceSerialization.kt | 114 ++++++++++++------ .../editor/EditorResourceSerializationTest.kt | 38 +++--- 2 files changed, 97 insertions(+), 55 deletions(-) diff --git a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceSerialization.kt b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceSerialization.kt index 63c0323cd..a6f08e102 100644 --- a/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceSerialization.kt +++ b/src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceSerialization.kt @@ -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 @@ -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. @@ -41,32 +42,62 @@ object EditorResourceSerialization { * @see JsonFileType.INSTANCE */ fun deserialize(jsonYaml: String?, fileType: FileType?, currentNamespace: String?): List { - 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(jsonYaml)) + } catch (e: RuntimeException) { + throw ResourceException("Invalid kubernetes yaml/json", e.cause ?: e) + } + } + + private fun yaml2Resources(yaml: String, currentNamespace: String?): List { + val resources = yaml + .split(RESOURCE_SEPARATOR_YAML) + .filter { yaml -> + yaml.isNotBlank() + } + return resources + .map { yaml -> + setMissingNamespace(currentNamespace, createResource(yaml)) + } + .toList() + } + + private fun json2Resources(json: String?, currentNamespace: String?): List { + 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 } @@ -74,27 +105,36 @@ object EditorResourceSerialization { } fun serialize(resources: List, 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): 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): String { + return if (resources.size == 1) { + Serialization.asJson(resources.first()).trim() + } else { + Serialization.asJson(resources).trim() } } diff --git a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceSerializationTest.kt b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceSerializationTest.kt index e2d789d65..e31de3e5a 100644 --- a/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceSerializationTest.kt +++ b/src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceSerializationTest.kt @@ -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 @@ -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 @@ -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("darth vader"), resource("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 @@ -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("leia") // when @@ -268,5 +271,4 @@ class EditorResourceSerializationTest { assertThat(serialized) .isEqualTo("") } - } \ No newline at end of file