Skip to content

Commit 98549ed

Browse files
committed
fix: dont trash the document when removing clutter (redhat-developer#818)
Signed-off-by: Andre Dietisheim <[email protected]>
1 parent c089bae commit 98549ed

File tree

7 files changed

+314
-27
lines changed

7 files changed

+314
-27
lines changed

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/EditorResourceSerialization.kt

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
package com.redhat.devtools.intellij.kubernetes.editor
1212

1313
import com.intellij.json.JsonFileType
14-
import com.intellij.openapi.editor.Document
1514
import com.intellij.openapi.fileTypes.FileType
1615
import com.redhat.devtools.intellij.kubernetes.model.util.ResourceException
1716
import com.redhat.devtools.intellij.kubernetes.model.util.createResource
@@ -26,10 +25,20 @@ object EditorResourceSerialization {
2625
private const val RESOURCE_SEPARATOR_JSON = ",\n"
2726

2827
/**
29-
* Returns a [HasMetadata] for a given [Document] instance.
28+
* Returns a list of [HasMetadata] for a given yaml or json string.
29+
* Several resources are only supported for yaml file type. Trying to deserialize ex. json that would result
30+
* in several resources throws a [ResourceException].
3031
*
31-
* @param jsonYaml serialized resources
32-
* @return [HasMetadata] for the given editor and clients
32+
* @param jsonYaml string representing kubernetes resources
33+
* @param fileType yaml- or json file type (no other types are supported)
34+
* @param currentNamespace the namespace to set to the resulting resources if they have none
35+
* @return list of [HasMetadata] for the given yaml or json string
36+
* @throws ResourceException if several resources are to be deserialized to a non-yaml filetype
37+
*
38+
* @see isSupported
39+
* @see RESOURCE_SEPARATOR_YAML
40+
* @see YAMLFileType.YML
41+
* @see JsonFileType.INSTANCE
3342
*/
3443
fun deserialize(jsonYaml: String?, fileType: FileType?, currentNamespace: String?): List<HasMetadata> {
3544
return if (jsonYaml == null
@@ -64,25 +73,27 @@ object EditorResourceSerialization {
6473
return resource
6574
}
6675

67-
fun serialize(resources: Collection<HasMetadata>, fileType: FileType?): String? {
76+
fun serialize(resources: List<HasMetadata>, fileType: FileType?): String? {
6877
if (fileType == null) {
6978
return null
7079
}
71-
if (resources.size >=2 && fileType != YAMLFileType.YML) {
80+
if (resources.size >= 2
81+
&& fileType != YAMLFileType.YML) {
7282
throw UnsupportedOperationException(
7383
"${fileType.name} is not supported for multi-resource documents. Only ${YAMLFileType.YML.name} is.")
7484
}
7585
return resources
7686
.mapNotNull { resource -> serialize(resource, fileType) }
77-
.joinToString(RESOURCE_SEPARATOR_YAML)
87+
.joinToString("\n")
88+
7889
}
7990

8091
private fun serialize(resource: HasMetadata, fileType: FileType): String? {
8192
return when(fileType) {
8293
YAMLFileType.YML ->
8394
Serialization.asYaml(resource).trim()
8495
JsonFileType.INSTANCE ->
85-
Serialization.asYaml(resource).trim()
96+
Serialization.asJson(resource).trim()
8697
else -> null
8798
}
8899
}

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ResourceEditor.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ open class ResourceEditor(
5151
// for mocking purposes
5252
private val createResources: (string: String?, fileType: FileType?, currentNamespace: String?) -> List<HasMetadata> =
5353
EditorResourceSerialization::deserialize,
54-
private val serialize: (resources: Collection<HasMetadata>, fileType: FileType?) -> String? =
54+
private val serialize: (resources: List<HasMetadata>, fileType: FileType?) -> String? =
5555
EditorResourceSerialization::serialize,
5656
// for mocking purposes
5757
private val createResourceFileForVirtual: (file: VirtualFile?) -> ResourceFile? =
@@ -165,7 +165,7 @@ open class ResourceEditor(
165165
}
166166
}
167167

168-
private fun replaceDocument(resources: Collection<HasMetadata>): Boolean {
168+
private fun replaceDocument(resources: List<HasMetadata>): Boolean {
169169
val manager = getPsiDocumentManager.invoke(project)
170170
val document = getDocument.invoke(editor) ?: return false
171171
val jsonYaml = serialize.invoke(resources, getFileType(document, manager)) ?: return false
@@ -269,17 +269,20 @@ open class ResourceEditor(
269269
}
270270

271271
fun removeClutter() {
272-
val resources = editorResources.getAllResources().map { resource ->
272+
val resources = createResources(
273+
getDocument(editor),
274+
editor.file.fileType) // don't insert namespace if not present (no namespace param)
275+
val cleaned = resources.map { resource ->
273276
MetadataClutter.remove(resource.metadata)
274277
resource
275278
}
276279
runInUI {
277-
replaceDocument(resources)
280+
replaceDocument(cleaned)
278281
notifications.hideAll()
279282
}
280283
}
281284

282-
private fun createResources(document: Document?, fileType: FileType?, namespace: String?): List<HasMetadata> {
285+
private fun createResources(document: Document?, fileType: FileType?, namespace: String? = null): List<HasMetadata> {
283286
return createResources.invoke(
284287
document?.text,
285288
fileType,
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Red Hat, Inc.
3+
* Distributed under license by Red Hat, Inc. All rights reserved.
4+
* This program is made available under the terms of the
5+
* Eclipse Public License v2.0 which accompanies this distribution,
6+
* and is available at http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* Contributors:
9+
* Red Hat, Inc. - initial API and implementation
10+
******************************************************************************/
11+
package com.redhat.devtools.intellij.kubernetes.editor
12+
13+
import com.intellij.json.JsonFileType
14+
import com.intellij.openapi.fileTypes.PlainTextFileType
15+
import com.redhat.devtools.intellij.kubernetes.model.mocks.ClientMocks.resource
16+
import com.redhat.devtools.intellij.kubernetes.model.util.ResourceException
17+
import io.fabric8.kubernetes.api.model.Pod
18+
import io.fabric8.kubernetes.client.utils.Serialization
19+
import org.assertj.core.api.Assertions.assertThat
20+
import org.assertj.core.api.Assertions.assertThatThrownBy
21+
import org.jetbrains.yaml.YAMLFileType
22+
import org.junit.Test
23+
24+
class EditorResourceSerializationTest {
25+
26+
@Test
27+
fun `#deserialize returns empty list if it is given null yaml`() {
28+
// given
29+
// when
30+
val deserialized = EditorResourceSerialization.deserialize(null, YAMLFileType.YML, "dagobah")
31+
// then
32+
assertThat(deserialized)
33+
.isEmpty()
34+
}
35+
36+
@Test
37+
fun `#deserialize returns a list of resources if given multi-resource YAML`() {
38+
// given
39+
val yaml = """
40+
apiVersion: v1
41+
kind: Pod
42+
metadata:
43+
name: yoda
44+
---
45+
apiVersion: v1
46+
kind: Service
47+
metadata:
48+
name: luke
49+
""".trimIndent()
50+
// when
51+
val deserialized = EditorResourceSerialization.deserialize(yaml, YAMLFileType.YML, "dagobah")
52+
// then
53+
assertThat(deserialized)
54+
.hasSize(2)
55+
.extracting("kind")
56+
.containsExactly("Pod", "Service")
57+
}
58+
59+
@Test
60+
fun `#deserialize throws if given multiple json resources`() {
61+
// given
62+
val json = """
63+
{"apiVersion": "v1", "kind": "Pod"}
64+
---
65+
{"apiVersion": "v1", "kind": "Service"}
66+
""".trimIndent()
67+
68+
assertThatThrownBy {
69+
// when
70+
EditorResourceSerialization.deserialize(json, JsonFileType.INSTANCE, null)
71+
// then
72+
}.isInstanceOf(ResourceException::class.java)
73+
}
74+
75+
@Test
76+
fun `#deserialize returns a list with a single resource if given valid JSON with a single resource`() {
77+
// given
78+
val json = """
79+
{
80+
"apiVersion": "v1",
81+
"kind": "Pod",
82+
"metadata": {
83+
"name": "obiwan"
84+
}
85+
}
86+
""".trimIndent()
87+
// when
88+
val deserialized = EditorResourceSerialization.deserialize(json, JsonFileType.INSTANCE, null)
89+
// then
90+
assertThat(deserialized)
91+
.singleElement()
92+
.extracting("kind")
93+
.isEqualTo("Pod")
94+
}
95+
96+
@Test
97+
fun `#deserialize sets the current namespace to the resulting resource if it has no namespace`() {
98+
// given
99+
val yaml = """
100+
apiVersion: v1
101+
kind: Pod
102+
metadata:
103+
name: has-no-namespace
104+
""".trimIndent()
105+
// when
106+
val deserialized = EditorResourceSerialization.deserialize(yaml, YAMLFileType.YML, "namespace-that-should-be-set")
107+
// then
108+
assertThat(deserialized)
109+
.first()
110+
.extracting { it.metadata.namespace }
111+
.isEqualTo("namespace-that-should-be-set")
112+
}
113+
114+
@Test
115+
fun `#deserialize sets the current namespace only to the resources that have no namespace`() {
116+
// given
117+
val yaml = """
118+
apiVersion: v1
119+
kind: Pod
120+
metadata:
121+
name: has-no-namespace
122+
---
123+
apiVersion: v1
124+
kind: Pod
125+
metadata:
126+
name: has-a-namespace
127+
namespace: alderaan
128+
""".trimIndent()
129+
// when
130+
val deserialized = EditorResourceSerialization.deserialize(
131+
yaml, YAMLFileType.YML, "namespace-that-should-be-set"
132+
)
133+
// then
134+
assertThat(deserialized)
135+
.satisfiesExactly(
136+
{ assertThat(it.metadata.namespace).isEqualTo("namespace-that-should-be-set") }, // namespace set
137+
{ assertThat(it.metadata.namespace).isEqualTo("alderaan") } // no namespace set
138+
)
139+
}
140+
141+
@Test
142+
fun `#deserialize does not change namespace in the resulting resource if it has a namespace`() {
143+
// given
144+
val yaml = """
145+
apiVersion: v1
146+
kind: Pod
147+
metadata:
148+
name: yoda
149+
namespace: has-a-namespace
150+
""".trimIndent()
151+
// when
152+
val deserialized = EditorResourceSerialization.deserialize(yaml, YAMLFileType.YML, "should-not-override-existing")
153+
// then
154+
assertThat(deserialized)
155+
.first()
156+
.extracting { it.metadata.namespace }
157+
.isEqualTo("has-a-namespace")
158+
}
159+
160+
@Test
161+
fun `#deserialize does not change namespace in the resulting resource if it has a namespace and no given namespace`() {
162+
// given
163+
val yaml = """
164+
apiVersion: v1
165+
kind: Pod
166+
metadata:
167+
name: yoda
168+
namespace: has-a-namespace
169+
""".trimIndent()
170+
// when
171+
val deserialized = EditorResourceSerialization.deserialize(yaml, YAMLFileType.YML, null) // no namespace provided, keep whatever exists
172+
// then
173+
assertThat(deserialized)
174+
.first()
175+
.extracting { it.metadata.namespace }
176+
.isEqualTo("has-a-namespace")
177+
}
178+
179+
@Test
180+
fun `#deserialize throws if given invalid yaml`() {
181+
// given
182+
val invalidYaml = """
183+
apiVersion: v1
184+
kind: Pod
185+
metadata: invalid
186+
""".trimIndent()
187+
assertThatThrownBy {
188+
// when
189+
EditorResourceSerialization.deserialize(invalidYaml, YAMLFileType.YML, "dagobah")
190+
// then
191+
}.isInstanceOf(ResourceException::class.java)
192+
}
193+
194+
@Test
195+
fun `#serialize returns null if given null file type`() {
196+
// given
197+
val resource = resource<Pod>("darth vader")
198+
// when
199+
val serialized = EditorResourceSerialization.serialize(listOf(resource), null)
200+
// then
201+
assertThat(serialized)
202+
.isNull()
203+
}
204+
205+
@Test
206+
fun `#serialize throws if given multiple resources and non-YAML file type`() {
207+
// given
208+
val resources = listOf(
209+
resource<Pod>("darth vader"),
210+
resource<Pod>("emperor")
211+
)
212+
assertThatThrownBy {
213+
// when
214+
EditorResourceSerialization.serialize(resources, JsonFileType.INSTANCE)
215+
// then
216+
}.isInstanceOf(UnsupportedOperationException::class.java)
217+
}
218+
219+
@Test
220+
fun `#serialize returns correct YAML if given single resource and YAML file type`() {
221+
// given
222+
val resource = resource<Pod>("obiwan")
223+
val expected = Serialization.asYaml(resource).trim()
224+
// when
225+
val serialized = EditorResourceSerialization.serialize(listOf(resource), YAMLFileType.YML)
226+
//then
227+
assertThat(serialized)
228+
.isEqualTo(expected)
229+
}
230+
231+
@Test
232+
fun `#serialize returns multiple YAML resources joined with newline if given 2 resources and YAML file type`() {
233+
// given
234+
val resources = listOf(
235+
resource<Pod>("leia"),
236+
resource<Pod>("luke")
237+
)
238+
val expected = resources
239+
.joinToString("\n") {
240+
Serialization.asYaml(it).trim()
241+
}
242+
// when
243+
val serialized = EditorResourceSerialization.serialize(resources, YAMLFileType.YML)
244+
// then
245+
assertThat(serialized)
246+
.isEqualTo(expected)
247+
}
248+
249+
@Test
250+
fun `#serialize returns JSON if given JSON file type`() {
251+
// given
252+
val resource = resource<Pod>("obiwan")
253+
val expected = Serialization.asJson(resource).trim()
254+
// when
255+
val serialized = EditorResourceSerialization.serialize(listOf(resource), JsonFileType.INSTANCE)
256+
// then
257+
assertThat(serialized)
258+
.isEqualTo(expected)
259+
}
260+
261+
@Test
262+
fun `#serialize returns null if given unsupported file type`() {
263+
// given
264+
val resource = resource<Pod>("leia")
265+
// when
266+
val serialized = EditorResourceSerialization.serialize(listOf(resource), PlainTextFileType.INSTANCE)
267+
// then
268+
assertThat(serialized)
269+
.isEqualTo("")
270+
}
271+
272+
}

0 commit comments

Comments
 (0)