project configuration DSL: fix schema for run configurations - use oneOf

This commit is contained in:
Vladimir Krivosheev
2019-03-20 16:32:18 +01:00
parent 9c7ee91c73
commit c5f023640d
3 changed files with 126 additions and 79 deletions
+69 -52
View File
@@ -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 {
@@ -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<ConfigurationFactory>) -> 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<ConfigurationFactory>,
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
}
@@ -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: <caret>
""".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("<caret>", "IntelliJIDEARulezzz"))
val element = file.findElementAt(position)
assertThat(element).isNotNull