From c5f023640d9a6a0a46c2897bfcadd5386fc54919 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Wed, 20 Mar 2019 15:48:54 +0100 Subject: [PATCH] project configuration DSL: fix schema for run configurations - use oneOf --- .../configuration-script/src/JsonBuilder.kt | 121 ++++++++++-------- .../RunConfigurationJsonSchemaGenerator.kt | 69 ++++++---- .../test/ConfigurationSchemaTest.kt | 15 +++ 3 files changed, 126 insertions(+), 79 deletions(-) diff --git a/plugins/configuration-script/src/JsonBuilder.kt b/plugins/configuration-script/src/JsonBuilder.kt index 20d1b6900813..7ac08e6608ed 100644 --- a/plugins/configuration-script/src/JsonBuilder.kt +++ b/plugins/configuration-script/src/JsonBuilder.kt @@ -15,29 +15,23 @@ internal inline fun StringBuilder.json(build: JsonObjectBuilder.() -> Unit): Str } @JsonBuilderDsl -internal class JsonObjectBuilder(private val builder: StringBuilder) { +internal class JsonObjectBuilder(private val builder: StringBuilder, private val isCompact: Boolean = false) { infix fun String.to(value: String) { - builder - .appendCommaIfNeed() - .jsonEscapedString(this) - .append(':') - .jsonEscapedString(value) + appendNameAndValue(this) { + builder.jsonEscapedString(value) + } } infix fun String.toUnescaped(value: String) { - builder - .appendCommaIfNeed() - .jsonEscapedString(this) - .append(':') - JsonUtil.escape(value, builder) + appendNameAndValue(this) { + JsonUtil.escape(value, builder) + } } infix fun String.to(value: Boolean) { - builder - .appendCommaIfNeed() - .jsonEscapedString(this) - .append(':') - .append(value) + appendNameAndValue(this) { + builder.append(value) + } } infix fun String.to(value: StringBuilder) { @@ -45,34 +39,27 @@ internal class JsonObjectBuilder(private val builder: StringBuilder) { return } - builder - .appendCommaIfNeed() - .jsonEscapedString(this) - .append(':') + appendNameAndValue(this) { // append as is - .append(value) + builder.append(value) + } } infix fun String.toRaw(value: String) { - builder - .appendCommaIfNeed() - .jsonEscapedString(this) - .append(':') + appendNameAndValue(this) { // append as is - .append(value) + builder.append(value) + } } fun map(key: CharSequence, build: JsonObjectBuilder.() -> Unit) { builder .appendCommaIfNeed() .jsonEscapedString(key) - .append(':') - .append('{') - .append('\n') - this.build() - builder - .append('\n') - .append('}') + + appendComplexValue('{', '}') { + build() + } } fun rawMap(key: CharSequence, build: (StringBuilder) -> Unit) { @@ -87,37 +74,67 @@ internal class JsonObjectBuilder(private val builder: StringBuilder) { builder .appendCommaIfNeed() .jsonEscapedString(key) - .append(':') - .append(openChar) - .append('\n') - build(builder) - builder - .append('\n') - .append(closeChar) + appendComplexValue(openChar, closeChar) { + build(builder) + } } fun rawBuilder(key: CharSequence, child: JsonObjectBuilder) { builder .appendCommaIfNeed() .jsonEscapedString(key) - .append(':') - .append('{') - .append('\n') - .append(child.builder) - .append('\n') - .append('}') + appendComplexValue('{', '}') { + builder.append(child.builder) + } } - fun definitionReference(prefix: String, pointer: CharSequence) { - builder - .appendCommaIfNeed() - .jsonEscapedString("\$ref") - .append(':') - .append('"') + fun definitionReference(prefix: String, pointer: CharSequence, wrappingKey: String? = null) { + builder.appendCommaIfNeed() + + if (wrappingKey != null) { + builder.append('"').append(wrappingKey).append('"').append(':').append(' ') + } + + builder.jsonEscapedString("\$ref") + appendColon() + builder.append('"') .append(prefix) .append(pointer) .append('"') } + + private fun appendColon() { + builder.append(':') + if (!isCompact) { + builder.append(' ') + } + } + + private inline fun appendNameAndValue(name: CharSequence, valueAppender: () -> Unit) { + builder + .appendCommaIfNeed() + .jsonEscapedString(name) + appendColon() + valueAppender() + } + + private inline fun appendComplexValue(openChar: Char, closeChar: Char, valueAppender: () -> Unit) { + appendColon() + builder.append(openChar) + if (!isCompact) { + builder + .append('\n') + .append(' ') + .append(' ') + } + + valueAppender() + + if (!isCompact) { + builder.append('\n') + } + builder.append(closeChar) + } } private fun StringBuilder.appendCommaIfNeed(): StringBuilder { diff --git a/plugins/configuration-script/src/RunConfigurationJsonSchemaGenerator.kt b/plugins/configuration-script/src/RunConfigurationJsonSchemaGenerator.kt index 7613b7bcc9d7..2fda15b6952d 100644 --- a/plugins/configuration-script/src/RunConfigurationJsonSchemaGenerator.kt +++ b/plugins/configuration-script/src/RunConfigurationJsonSchemaGenerator.kt @@ -9,8 +9,7 @@ import com.intellij.openapi.util.text.StringUtil import com.intellij.util.ReflectionUtil internal inline fun processConfigurationTypes(processor: (configurationType: ConfigurationType, propertyName: CharSequence, factories: Array) -> Unit) { - val configurationTypes = ConfigurationType.CONFIGURATION_TYPE_EP.extensionList - for (type in configurationTypes) { + for (type in ConfigurationType.CONFIGURATION_TYPE_EP.extensionList) { val propertyName = rcTypeIdToPropertyName(type) ?: continue val factories = type.configurationFactories if (factories.isEmpty()) { @@ -27,7 +26,7 @@ private inline fun processFactories(factories: Array, processor: (factoryPropertyName: CharSequence, factoryDefinitionId: CharSequence, factory: ConfigurationFactory) -> Unit) { for (factory in factories) { val factoryPropertyName = rcFactoryIdToPropertyName(factory) ?: continue - val factoryDefinitionId = "${typeDefinitionId}-${factoryPropertyName}" + val factoryDefinitionId = "${typeDefinitionId}-${factoryPropertyName}Factory" processor(factoryPropertyName, factoryDefinitionId, factory) } } @@ -44,18 +43,22 @@ internal class RunConfigurationJsonSchemaGenerator { addTemplatesNode(properties) processConfigurationTypes { type, typePropertyName, factories -> - @Suppress("UnnecessaryVariable") - val typeDefinitionId = typePropertyName - val isMultiFactory = factories.size > 1 - addPropertyForConfigurationType(properties, typePropertyName, isMultiFactory, typeDefinitionId) + val typeDefinitionId = generateTypeDefinitionId(typePropertyName) + val typeDescription = getTypeDescription(type, typePropertyName) + + addPropertyForConfigurationType(properties, typePropertyName, isMultiFactory, typeDefinitionId, typeDescription) if (isMultiFactory) { + processFactories(factories, typeDefinitionId) { factoryPropertyName, factoryDefinitionId, factory -> + describeFactory(factory, factoryDefinitionId, if (StringUtil.equals(factoryPropertyName, factory.name)) null else factory.name) + } + definitions.map(typeDefinitionId) { "type" to "object" - if (!StringUtil.equals(typePropertyName, type.configurationTypeDescription)) { - "description" toUnescaped type.configurationTypeDescription + if (typeDescription != null) { + "description" toUnescaped typeDescription } map("properties") { @@ -64,15 +67,12 @@ internal class RunConfigurationJsonSchemaGenerator { // describeFactory cannot be here because JsonBuilder instance here equals to definitions - recursive building is not supported (to reuse StringBuilder instance) } } - } - processFactories(factories, typeDefinitionId) { factoryPropertyName, factoryDefinitionId, factory -> - describeFactory(factory, factoryDefinitionId, if (StringUtil.equals(factoryPropertyName, factory.name)) null else factory.name) + "additionalProperties" to false } } else { - val description = type.configurationTypeDescription - describeFactory(factories.first(), typeDefinitionId, if (typePropertyName == description) null else description) + describeFactory(factories.first(), typeDefinitionId, typeDescription) } } @@ -97,20 +97,22 @@ internal class RunConfigurationJsonSchemaGenerator { "description" toUnescaped description map("properties") { processConfigurationTypes { type, typePropertyName, factories -> + val typeDefinitionId = generateTypeDefinitionId(typePropertyName) + val typeDescription = getTypeDescription(type, typePropertyName) if (factories.size == 1) { - addPropertyForConfigurationType(this, typePropertyName, true, typePropertyName) + addPropertyForConfigurationType(this, typePropertyName, true, typeDefinitionId, typeDescription) } else { // for multi-factory RC type we cannot simply reference to definition because the only child is expected (RC type cannot have more than one template) map(typePropertyName) { "type" to "object" - if (!StringUtil.equals(typePropertyName, type.configurationTypeDescription)) { - "description" toUnescaped type.configurationTypeDescription + if (typeDescription != null) { + "description" toUnescaped typeDescription } map("properties") { - processFactories(factories, typePropertyName) { factoryPropertyName, factoryDefinitionId, _ -> + processFactories(factories, typeDefinitionId) { factoryPropertyName, factoryDefinitionId, _ -> addPropertyForFactory(factoryPropertyName, factoryDefinitionId, isSingleChildOnly = true) } } @@ -121,22 +123,26 @@ internal class RunConfigurationJsonSchemaGenerator { } } - private fun addPropertyForConfigurationType(properties: JsonObjectBuilder, typePropertyName: CharSequence, isSingleChildOnly: Boolean, definitionId: CharSequence) { + private fun addPropertyForConfigurationType(properties: JsonObjectBuilder, typePropertyName: CharSequence, isSingleChildOnly: Boolean, definitionId: CharSequence, typeDescription: String?) { properties.map(typePropertyName) { if (isSingleChildOnly) { "type" to "object" + definitionReference(definitionPointerPrefix, definitionId) } else { - "type" toRaw """["array", "object"]""" - } - - if (!isSingleChildOnly) { - map("items") { - definitionReference(definitionPointerPrefix, definitionId) + rawArray("oneOf") { + it.json { + definitionReference(definitionPointerPrefix, definitionId) + } + it.json { + "type" to "array" + map("items") { + definitionReference(definitionPointerPrefix, definitionId) + } + "additionalProperties" to false + } } } - - definitionReference(definitionPointerPrefix, definitionId) } } @@ -299,4 +305,13 @@ private fun idToPropertyName(string: String, configurationType: ConfigurationTyp else { return builder ?: result } +} + +private fun generateTypeDefinitionId(propertyName: CharSequence): String { + return "${propertyName[0].toUpperCase()}${propertyName.substring(1)}Type" +} + +private fun getTypeDescription(type: ConfigurationType, typePropertyName: CharSequence): String? { + val description = type.configurationTypeDescription + return if (StringUtil.equals(typePropertyName, description)) null else description } \ No newline at end of file diff --git a/plugins/configuration-script/test/ConfigurationSchemaTest.kt b/plugins/configuration-script/test/ConfigurationSchemaTest.kt index 4379e37cd760..4db9910a8c22 100644 --- a/plugins/configuration-script/test/ConfigurationSchemaTest.kt +++ b/plugins/configuration-script/test/ConfigurationSchemaTest.kt @@ -33,6 +33,20 @@ internal class ConfigurationSchemaTest : CompletionTestCase() { checkDescription(variants, "isShowConsoleOnStdOut", "Show console when a message is printed to standard output stream") } + fun `test array or object`() { + val variants = test(""" + runConfigurations: + java: + """.trimIndent()) + + val texts = variants.map { + val presentation = LookupElementPresentation() + it.renderElement(presentation) + presentation.itemText + } + assertThat(texts).contains("{...}", "[...]") + } + fun `test no isAllowRunningInParallel if singleton policy not configurable`() { val variants = test(""" runConfigurations: @@ -56,6 +70,7 @@ internal class ConfigurationSchemaTest : CompletionTestCase() { val position = EditorTestUtil.getCaretPosition(text) assertThat(position).isGreaterThan(0) + @Suppress("SpellCheckingInspection") val file = createFile(myModule, "intellij.yaml", text.replace("", "IntelliJIDEARulezzz")) val element = file.findElementAt(position) assertThat(element).isNotNull