From 74c3c506d97fd981c7c4400194f709abd79d32c9 Mon Sep 17 00:00:00 2001 From: tangcent Date: Sat, 12 Oct 2024 08:38:22 +0800 Subject: [PATCH] feat: add 'yapi.no_update.description' to stop api description updates --- .../api/export/yapi/DefaultYapiApiHelper.kt | 2 +- .../plugin/api/export/yapi/YapiApiHelper.kt | 31 ++- .../api/export/yapi/YapiSaveInterceptor.kt | 47 ++++- ...plugin.api.export.yapi.YapiSaveInterceptor | 1 + .../export/condition/OnDocConditionTest.kt | 1 + .../export/yapi/YapiSaveInterceptorTest.kt | 194 ++++++++++++------ 6 files changed, 191 insertions(+), 85 deletions(-) diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/DefaultYapiApiHelper.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/DefaultYapiApiHelper.kt index 6f1cac54b..7e5b2b0a7 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/DefaultYapiApiHelper.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/DefaultYapiApiHelper.kt @@ -78,7 +78,7 @@ open class DefaultYapiApiHelper : AbstractYapiApiHelper(), YapiApiHelper { } override fun saveApiInfo(apiInfo: HashMap): Boolean { - if (!saveInterceptor.beforeSaveApi(this, apiInfo)) { + if (saveInterceptor.beforeSaveApi(this, apiInfo) == false) { return false } diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiApiHelper.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiApiHelper.kt index 696a87314..7d9e15b41 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiApiHelper.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiApiHelper.kt @@ -58,20 +58,29 @@ fun YapiApiHelper.listApis(token: String, catId: String): JsonArray? { } fun YapiApiHelper.existed(apiInfo: HashMap): Boolean { - val path = apiInfo["path"] ?: return false - val method = apiInfo["method"] ?: return false - val token = apiInfo["token"] as? String ?: return false - val projectId: String = this.getProjectIdByToken(token) ?: return false - val carts = this.findCarts(projectId, token) ?: return false + return this.findExistApi(apiInfo) != null +} + +fun YapiApiHelper.findExistApi(apiInfo: HashMap): JsonObject? { + val path = apiInfo["path"] as? String ?: return null + val method = apiInfo["method"] as? String ?: return null + val token = apiInfo["token"] as? String ?: return null + return this.findExistApi(token, path, method) +} + +fun YapiApiHelper.findExistApi(token: String, path: String, method: String): JsonObject? { + val projectId: String = this.getProjectIdByToken(token) ?: return null + val carts = this.findCarts(projectId, token) ?: return null for (cart in carts) { - val cart_id = (cart as? Map<*, *>)?.get("_id")?.toString() ?: continue - if (this.listApis(token, cart_id, null)?.any { api -> - api.sub("path")?.asString == path && api.sub("method")?.asString == method - } == true) { - return true + val catId = (cart as? Map<*, *>)?.get("_id")?.toString() ?: continue + val api = this.listApis(token, catId, null)?.find { api -> + api.sub("path")?.asString == path && api.sub("method")?.asString == method + } + if (api != null) { + return api as JsonObject } } - return false + return null } fun YapiApiHelper.getCartForFolder(folder: Folder, privateToken: String): CartInfo? { diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSaveInterceptor.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSaveInterceptor.kt index 49e2bddde..d8ec08ab5 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSaveInterceptor.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSaveInterceptor.kt @@ -2,9 +2,12 @@ package com.itangcent.idea.plugin.api.export.yapi import com.intellij.openapi.ui.Messages import com.itangcent.common.concurrent.ValueHolder +import com.itangcent.common.utils.toBool import com.itangcent.idea.plugin.condition.ConditionOnSetting import com.itangcent.idea.swing.MessagesHelper +import com.itangcent.intellij.config.ConfigReader import com.itangcent.intellij.context.ActionContext +import com.itangcent.intellij.extend.sub /** * Workflow interface that allows for customized yapi save action. @@ -13,10 +16,10 @@ internal interface YapiSaveInterceptor { /** * Called before [YapiApiHelper] save an apiInfo to yapi server. * - * @return @return {@code true} if the apiInfo should be saved. - * Else, [YapiApiHelper] will discard this apiInfo. + * @return return {@code false} [YapiApiHelper] will discard this apiInfo. + * else [YapiApiHelper] will save this apiInfo to yapi server. */ - fun beforeSaveApi(apiHelper: YapiApiHelper, apiInfo: HashMap): Boolean + fun beforeSaveApi(apiHelper: YapiApiHelper, apiInfo: HashMap): Boolean? } /** @@ -57,7 +60,7 @@ class AlwaysAskYapiSaveInterceptor : YapiSaveInterceptor { private var selectedYapiSaveInterceptor: YapiSaveInterceptor? = null @Synchronized - override fun beforeSaveApi(apiHelper: YapiApiHelper, apiInfo: HashMap): Boolean { + override fun beforeSaveApi(apiHelper: YapiApiHelper, apiInfo: HashMap): Boolean? { if (selectedYapiSaveInterceptor != null) { return selectedYapiSaveInterceptor!!.beforeSaveApi(apiHelper, apiInfo) } @@ -68,7 +71,8 @@ class AlwaysAskYapiSaveInterceptor : YapiSaveInterceptor { val context = ActionContext.getContext() ?: return true context.instance(MessagesHelper::class).showAskWithApplyAllDialog( "The api [${apiInfo["title"]}] already existed in the project.\n" + - "Do you want update it?", arrayOf("Update", "Skip", "Cancel")) { ret, applyAll -> + "Do you want update it?", arrayOf("Update", "Skip", "Cancel") + ) { ret, applyAll -> if (ret == Messages.CANCEL) { context.stop() valueHolder.success(false) @@ -91,3 +95,36 @@ class AlwaysAskYapiSaveInterceptor : YapiSaveInterceptor { return valueHolder.value() ?: false } } + +/** + * A [YapiSaveInterceptor] that prevents overwriting the existing description and markdown fields in YAPI when saving API details. + * Only works when the configuration "yapi.no_update.description" is set to true. + */ +class NoUpdateDescriptionYapiSaveInterceptor : YapiSaveInterceptor { + override fun beforeSaveApi(apiHelper: YapiApiHelper, apiInfo: HashMap): Boolean? { + recoverDescription(apiHelper, apiInfo) + return null + } + + /** + * Retrieves the existing API information from YAPI and copies the description and markdown + * fields to the new API information to prevent them from being overwritten. + */ + private fun recoverDescription(apiHelper: YapiApiHelper, apiInfo: HashMap) { + val disable = ActionContext.getContext() + ?.instance(ConfigReader::class) + ?.first("yapi.no_update.description") + ?.toBool(false) ?: false + if (!disable) return + + val existedApi = apiHelper.findExistApi(apiInfo) ?: return + val apiId = existedApi.sub("_id")?.asString ?: return + val existedApiInfo = apiHelper.getApiInfo(apiInfo["token"] as String, apiId) + + val existedDescription = existedApiInfo.sub("desc")?.asString ?: "" + apiInfo["desc"] = existedDescription + + val existedMarkdown = existedApiInfo.sub("markdown")?.asString ?: "" + apiInfo["markdown"] = existedMarkdown + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/resources/META-INF/services/com.itangcent.idea.plugin.api.export.yapi.YapiSaveInterceptor b/idea-plugin/src/main/resources/META-INF/services/com.itangcent.idea.plugin.api.export.yapi.YapiSaveInterceptor index 8ec229151..ae8eaaecb 100644 --- a/idea-plugin/src/main/resources/META-INF/services/com.itangcent.idea.plugin.api.export.yapi.YapiSaveInterceptor +++ b/idea-plugin/src/main/resources/META-INF/services/com.itangcent.idea.plugin.api.export.yapi.YapiSaveInterceptor @@ -1,3 +1,4 @@ com.itangcent.idea.plugin.api.export.yapi.AlwaysUpdateYapiSaveInterceptor com.itangcent.idea.plugin.api.export.yapi.NeverUpdateYapiSaveInterceptor com.itangcent.idea.plugin.api.export.yapi.AlwaysAskYapiSaveInterceptor +com.itangcent.idea.plugin.api.export.yapi.NoUpdateDescriptionYapiSaveInterceptor diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/condition/OnDocConditionTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/condition/OnDocConditionTest.kt index 67f1a88ab..140668319 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/condition/OnDocConditionTest.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/condition/OnDocConditionTest.kt @@ -1,6 +1,7 @@ package com.itangcent.idea.plugin.api.export.condition import com.itangcent.idea.plugin.api.export.ExportDoc +import com.itangcent.idea.plugin.api.export.condition.ConditionOnChannel import com.itangcent.intellij.context.ActionContext import com.itangcent.mock.AdvancedContextTest import org.junit.jupiter.api.Test diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSaveInterceptorTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSaveInterceptorTest.kt index c2f4b2626..5ceb7a2b2 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSaveInterceptorTest.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiSaveInterceptorTest.kt @@ -7,7 +7,11 @@ import com.itangcent.idea.plugin.settings.SettingBinder import com.itangcent.idea.plugin.settings.Settings import com.itangcent.idea.plugin.settings.YapiExportMode import com.itangcent.idea.swing.MessagesHelper +import com.itangcent.intellij.config.ConfigReader import com.itangcent.intellij.context.ActionContext +import com.itangcent.intellij.extend.guice.singleton +import com.itangcent.intellij.extend.guice.with +import com.itangcent.intellij.extend.sub import com.itangcent.mock.BaseContextTest import com.itangcent.mock.SettingBinderAdaptor import com.itangcent.spi.SpiCompositeLoader @@ -16,8 +20,7 @@ import org.junit.jupiter.api.Test import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.mock -import kotlin.test.assertFalse -import kotlin.test.assertTrue +import kotlin.test.assertEquals /** * Test case of [YapiSaveInterceptor] @@ -26,84 +29,113 @@ internal class YapiSaveInterceptorTest : BaseContextTest() { private val settings = Settings() - private val yapiApiHelper: YapiApiHelper = mock() - private val messagesHelper: MessagesHelper = mock() + private lateinit var yapiApiHelper: YapiApiHelper + private lateinit var messagesHelper: MessagesHelper + + private var answerTask = Messages.YES + private var answerApplyAll = false + + private val api1: HashMap = hashMapOf( + "_id" to 1, + "name" to "test api", + "path" to "/test", + "method" to "GET", + "token" to "123", + "desc" to "test api description", + "markdown" to "test api markdown" + ) + + private val api2: HashMap = hashMapOf( + "_id" to 2, + "name" to "test api 2", + "path" to "/test2", + "method" to "POST", + "token" to "123", + "desc" to "test api description 2", + "markdown" to "test api markdown 2" + ) override fun bind(builder: ActionContext.ActionContextBuilder) { super.bind(builder) builder.bind(SettingBinder::class) { it.toInstance(SettingBinderAdaptor(settings)) } + builder.bind(ConfigReader::class) { + it.with(com.itangcent.idea.plugin.config.EnhancedConfigReader::class).singleton() + } + + yapiApiHelper = mock() + val apis = JsonArray() + val api = JsonObject() + api1.forEach { (k, v) -> api.addProperty(k, v?.toString()) } + apis.add(api) + Mockito.`when`(yapiApiHelper.getProjectIdByToken(any())) + .thenReturn("123") + Mockito.`when`(yapiApiHelper.findCarts(any(), any())) + .thenReturn(arrayListOf(mapOf("_id" to 1), mapOf("_id" to 2))) + Mockito.`when`( + yapiApiHelper.listApis( + com.itangcent.mock.any(""), + com.itangcent.mock.any(""), + Mockito.any() + ) + ).thenReturn(apis) + Mockito.`when`(yapiApiHelper.getApiInfo(any(), any())) + .thenAnswer { + val id = it.getArgument(1, String::class.java) + return@thenAnswer apis.find { api -> api.sub("_id")?.asString == id } + } + builder.bind(YapiApiHelper::class) { it.toInstance(yapiApiHelper) } + + messagesHelper = mock() + Mockito.`when`( + messagesHelper.showAskWithApplyAllDialog( + Mockito.any(), + Mockito.any(), + com.itangcent.mock.any { _, _ -> }) + ).thenAnswer { + val callBack: (Int, Boolean) -> Unit = it.getArgument(2)!! + callBack(answerTask, answerApplyAll) + } + builder.bind(MessagesHelper::class) { it.toInstance(messagesHelper) } } @Test - fun beforeSaveApi() { - val api1: HashMap = hashMapOf( - "name" to "test api", - "path" to "/test", - "method" to "GET", - "token" to "123" - ) - val api2: HashMap = hashMapOf( - "name" to "test api 2", - "path" to "/test2", - "method" to "POST", - "token" to "123" - ) - var answerTask = Messages.YES - var answerApplyAll = false - run { - val apis = JsonArray() - val api = JsonObject() - api1.forEach { (k, v) -> api.addProperty(k, v?.toString()) } - apis.add(api) - Mockito.`when`(yapiApiHelper.getProjectIdByToken(any())) - .thenReturn("123") - Mockito.`when`(yapiApiHelper.findCarts(any(), any())) - .thenReturn(arrayListOf(mapOf("_id" to 1))) - Mockito.`when`(yapiApiHelper.listApis(com.itangcent.mock.any(""), - com.itangcent.mock.any(""), - Mockito.any())) - .thenReturn(apis) - - Mockito.doAnswer { - val callBack: (Int, Boolean) -> Unit = it.getArgument(2)!! - callBack(answerTask, answerApplyAll) - }.`when`(messagesHelper) - .showAskWithApplyAllDialog(Mockito.any(), Mockito.any(), com.itangcent.mock.any { _, _ -> }) - } + fun `test AlwaysUpdateYapiSaveInterceptor`() { + settings.yapiExportMode = YapiExportMode.ALWAYS_UPDATE.name + val saveInterceptor = SpiCompositeLoader.loadComposite() + assertEquals(true, saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) + assertEquals(true, saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) + } - run { - settings.yapiExportMode = YapiExportMode.ALWAYS_UPDATE.name - val saveInterceptor = SpiCompositeLoader.loadComposite() - assertTrue(saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) - assertTrue(saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) - } + @Test + fun `test NeverUpdateYapiSaveInterceptor`() { + settings.yapiExportMode = YapiExportMode.NEVER_UPDATE.name + val saveInterceptor = SpiCompositeLoader.loadComposite() - run { - settings.yapiExportMode = YapiExportMode.NEVER_UPDATE.name - val saveInterceptor = SpiCompositeLoader.loadComposite() + answerTask = Messages.YES + assertEquals(false, saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) + assertEquals(true, saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) - answerTask = Messages.YES - assertFalse(saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) - assertTrue(saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) + answerTask = Messages.NO + assertEquals(false, saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) + assertEquals(true, saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) + } - answerTask = Messages.NO - assertFalse(saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) - assertTrue(saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) - } + @Test + fun `test AlwaysAskYapiSaveInterceptor`() { run { settings.yapiExportMode = YapiExportMode.ALWAYS_ASK.name val saveInterceptor = SpiCompositeLoader.loadComposite() answerTask = Messages.YES - assertTrue(saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) - assertTrue(saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) + assertEquals(true, saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) + assertEquals(true, saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) answerTask = Messages.NO - assertFalse(saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) - assertTrue(saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) + assertEquals(false, saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) + assertEquals(true, saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) } run { @@ -112,13 +144,13 @@ internal class YapiSaveInterceptorTest : BaseContextTest() { answerTask = Messages.YES answerApplyAll = true - assertTrue(saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) - assertTrue(saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) + assertEquals(true, saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) + assertEquals(true, saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) answerTask = Messages.NO answerApplyAll = true - assertTrue(saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) - assertTrue(saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) + assertEquals(true, saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) + assertEquals(true, saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) } run { @@ -127,13 +159,13 @@ internal class YapiSaveInterceptorTest : BaseContextTest() { answerTask = Messages.NO answerApplyAll = true - assertFalse(saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) - assertTrue(saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) + assertEquals(false, saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) + assertEquals(true, saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) answerTask = Messages.YES answerApplyAll = true - assertFalse(saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) - assertTrue(saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) + assertEquals(false, saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) + assertEquals(true, saveInterceptor.beforeSaveApi(yapiApiHelper, api2)) } run { @@ -142,8 +174,34 @@ internal class YapiSaveInterceptorTest : BaseContextTest() { answerTask = Messages.CANCEL answerApplyAll = false - assertFalse(saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) + assertEquals(false, saveInterceptor.beforeSaveApi(yapiApiHelper, api1)) WaitHelper.waitUtil(5000) { actionContext.isStopped() } } } + + @Test + fun `test NoUpdateDescriptionYapiSaveInterceptor`() { + settings.yapiExportMode = YapiExportMode.ALWAYS_UPDATE.name + settings.builtInConfig = """ + yapi.no_update.description=true + """.trimIndent() + + val saveInterceptor = NoUpdateDescriptionYapiSaveInterceptor() + + val apiInfo = hashMapOf( + "name" to "test api", + "path" to "/test", + "method" to "GET", + "token" to "123", + "desc" to "New description", + "markdown" to "New markdown" + ) + + // Run interceptor + assertEquals(null, saveInterceptor.beforeSaveApi(yapiApiHelper, apiInfo)) + + // Assert that the existing description and markdown are retained + assertEquals("test api description", apiInfo["desc"]) + assertEquals("test api markdown", apiInfo["markdown"]) + } } \ No newline at end of file