diff --git a/generator/build.gradle.kts b/generator/build.gradle.kts index 7a1b499a5..5065e6d9f 100644 --- a/generator/build.gradle.kts +++ b/generator/build.gradle.kts @@ -34,6 +34,8 @@ dependencies { testRuntimeOnly(libs.junitEngine) testImplementation(libs.hamcrest) + testImplementation(libs.diffutils) + testImplementation(libs.cliktMarkdown) testImplementation(libs.dockerJava) testImplementation(libs.dockerJavaTransport) diff --git a/generator/src/main/kotlin/io/outfoxx/sunday/generator/kotlin/KotlinTypeRegistry.kt b/generator/src/main/kotlin/io/outfoxx/sunday/generator/kotlin/KotlinTypeRegistry.kt index 94d99ed38..99478de4e 100644 --- a/generator/src/main/kotlin/io/outfoxx/sunday/generator/kotlin/KotlinTypeRegistry.kt +++ b/generator/src/main/kotlin/io/outfoxx/sunday/generator/kotlin/KotlinTypeRegistry.kt @@ -512,8 +512,13 @@ class KotlinTypeRegistry( private fun processArrayShape(shape: ArrayShape, context: KotlinResolutionContext): TypeName { - val elementType = resolveReferencedTypeName(shape.items!!, context) - + val elementType = + shape.items + ?.let { itemsShape -> + resolveReferencedTypeName(itemsShape, context) + } + ?: ANY + val collectionType = if (shape.uniqueItems == true) { SET diff --git a/generator/src/main/kotlin/io/outfoxx/sunday/generator/swift/SwiftTypeRegistry.kt b/generator/src/main/kotlin/io/outfoxx/sunday/generator/swift/SwiftTypeRegistry.kt index cb72450fa..cedd84330 100644 --- a/generator/src/main/kotlin/io/outfoxx/sunday/generator/swift/SwiftTypeRegistry.kt +++ b/generator/src/main/kotlin/io/outfoxx/sunday/generator/swift/SwiftTypeRegistry.kt @@ -538,7 +538,12 @@ class SwiftTypeRegistry( private fun processArrayShape(shape: ArrayShape, context: SwiftResolutionContext): TypeName { - val elementType = resolveReferencedTypeName(shape.items!!, context) + val elementType = + shape.items + ?.let { itemsShape -> + resolveReferencedTypeName(itemsShape, context) + } + ?: ANY val collectionType = if (shape.uniqueItems == true) { @@ -917,42 +922,54 @@ class SwiftTypeRegistry( } val decoderPropertyRef = CodeBlock.of("self.%N", prop.swiftIdentifierName) - var decoderPost = "" + val decoderPost = CodeBlock.builder() var encoderPropertyRef = CodeBlock.of("self.%N", prop.swiftIdentifierName) - var encoderPre = "" + val encoderPre = CodeBlock.builder() val isLeaf = context.hasNoInheriting(prop.range) val (refCollection, refElement) = replaceCollectionValueTypesWithReferenceTypes(propertyTypeName) - if (!isLeaf) { - propertyTypeName = if (isOptional) { - (propertyTypeName.makeNonOptional() as DeclaredTypeName).nestedType(ANY_REF_NAME).makeOptional() - } else { - (propertyTypeName as DeclaredTypeName).nestedType(ANY_REF_NAME) + when { + !isLeaf -> { + propertyTypeName = if (isOptional) { + (propertyTypeName.makeNonOptional() as DeclaredTypeName).nestedType(ANY_REF_NAME).makeOptional() + } else { + (propertyTypeName as DeclaredTypeName).nestedType(ANY_REF_NAME) + } + + decoderPost.add("${if (isOptional) "?" else ""}.value") + } + + refCollection != propertyTypeName -> { + + val mapper = if (refCollection.concreteType() == DICTIONARY) "mapValues" else "map" + + propertyTypeName = refCollection + decoderPost.add("${if (isOptional) "?" else ""}.$mapper { $0.value }") + encoderPre.add("${if (isOptional) "?" else ""}.$mapper { %T(value: $0) }", refElement) } - decoderPost = "${if (isOptional) "?" else ""}.value" - } else if (refCollection != propertyTypeName) { - val mapper = if (refCollection.concreteType() == DICTIONARY) "mapValues" else "map" - propertyTypeName = refCollection - decoderPost = "${if (isOptional) "?" else ""}.$mapper { $0.value }" - encoderPre = "${if (isOptional) "?" else ""}.$mapper { ${refElement.name}(value: $0) }" - } else if (propertyTypeName == DICTIONARY_STRING_ANY || propertyTypeName == DICTIONARY_STRING_ANY_OPTIONAL) { - - propertyTypeName = DICTIONARY.parameterizedBy(STRING, ANY_VALUE) - decoderPost = "${if (isOptional) "?" else ""}.mapValues { $0.unwrapped as Any }" - encoderPre = "${if (isOptional) "?" else ""}.mapValues { try AnyValue.wrapped($0) }" - } else if (propertyTypeName == ARRAY_ANY || propertyTypeName == ARRAY_ANY_OPTIONAL) { - - propertyTypeName = ARRAY.parameterizedBy(ANY_VALUE) - decoderPost = "${if (isOptional) "?" else ""}.map { $0.unwrapped }" - encoderPre = "${if (isOptional) "?" else ""}.map { try AnyValue.wrapped($0) }" - } else if (propertyTypeName.unwrapOptional() == ANY) { - - propertyTypeName = ANY_VALUE - decoderPost = "${if (isOptional) "?" else ""}.unwrapped" - encoderPropertyRef = CodeBlock.of("%T.wrapped(%N)", ANY_VALUE, prop.swiftIdentifierName) + propertyTypeName == DICTIONARY_STRING_ANY || propertyTypeName == DICTIONARY_STRING_ANY_OPTIONAL -> { + + propertyTypeName = DICTIONARY.parameterizedBy(STRING, ANY_VALUE) + decoderPost.add("${if (isOptional) "?" else ""}.mapValues { $0.unwrapped }") + encoderPre.add("${if (isOptional) "?" else ""}.mapValues { try %T.wrapped($0) }", ANY_VALUE) + } + + propertyTypeName == ARRAY_ANY || propertyTypeName == ARRAY_ANY_OPTIONAL -> { + + propertyTypeName = ARRAY.parameterizedBy(ANY_VALUE) + decoderPost.add("${if (isOptional) "?" else ""}.map { $0.unwrapped }") + encoderPre.add("${if (isOptional) "?" else ""}.map { try %T.wrapped($0) }", ANY_VALUE) + } + + propertyTypeName.unwrapOptional() == ANY -> { + + propertyTypeName = ANY_VALUE + decoderPost.add("${if (isOptional) "?" else ""}.unwrapped") + encoderPropertyRef = CodeBlock.of("%T.wrapped(%N)", ANY_VALUE, prop.swiftIdentifierName) + } } decoderInitFunctionBuilder @@ -963,7 +980,7 @@ class SwiftTypeRegistry( coderSuffix, propertyTypeName, prop.swiftIdentifierName, - decoderPost, + decoderPost.build(), ) encoderFunctionBuilder @@ -971,7 +988,7 @@ class SwiftTypeRegistry( .addCode(encoderPropertyRef) .addCode( "%L, forKey: .%N)%]\n", - encoderPre, + encoderPre.build(), prop.swiftIdentifierName, ) } @@ -1131,9 +1148,9 @@ class SwiftTypeRegistry( paramType.makeOptional() } paramConsBuilder.addParameter( - ParameterSpec.builder(it.swiftIdentifierName, paramType) - .apply { if (it.optional && paramType.optional) defaultValue("nil") } - .build() + ParameterSpec.builder(it.swiftIdentifierName, paramType) + .apply { if (it.optional && paramType.optional) defaultValue("nil") } + .build(), ) } diff --git a/generator/src/main/kotlin/io/outfoxx/sunday/generator/typescript/TypeScriptTypeRegistry.kt b/generator/src/main/kotlin/io/outfoxx/sunday/generator/typescript/TypeScriptTypeRegistry.kt index b2c27e412..16964da1f 100644 --- a/generator/src/main/kotlin/io/outfoxx/sunday/generator/typescript/TypeScriptTypeRegistry.kt +++ b/generator/src/main/kotlin/io/outfoxx/sunday/generator/typescript/TypeScriptTypeRegistry.kt @@ -584,7 +584,12 @@ class TypeScriptTypeRegistry( private fun processArrayShape(shape: ArrayShape, context: TypeScriptResolutionContext): TypeName { - val elementType = resolveReferencedTypeName(shape.items!!, context) + val elementType = + shape.items + ?.let { itemsShape -> + resolveReferencedTypeName(itemsShape, context) + } + ?: UNKNOWN val collectionType = if (shape.uniqueItems == true) { diff --git a/generator/src/test/kotlin/io/outfoxx/sunday/generator/kotlin/KotlinTest.kt b/generator/src/test/kotlin/io/outfoxx/sunday/generator/kotlin/KotlinTest.kt index 11b4969e5..a4cadc68c 100644 --- a/generator/src/test/kotlin/io/outfoxx/sunday/generator/kotlin/KotlinTest.kt +++ b/generator/src/test/kotlin/io/outfoxx/sunday/generator/kotlin/KotlinTest.kt @@ -16,6 +16,7 @@ package io.outfoxx.sunday.generator.kotlin +import io.outfoxx.sunday.test.extensions.DiffingExtension import io.outfoxx.sunday.test.extensions.ResourceExtension import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.parallel.Execution @@ -24,5 +25,5 @@ import org.junit.jupiter.api.parallel.ResourceLock @Execution(ExecutionMode.CONCURRENT) @ResourceLock("Kotlin") -@ExtendWith(ResourceExtension::class) +@ExtendWith(ResourceExtension::class, DiffingExtension::class) annotation class KotlinTest diff --git a/generator/src/test/kotlin/io/outfoxx/sunday/generator/kotlin/RamlObjectTypesTest.kt b/generator/src/test/kotlin/io/outfoxx/sunday/generator/kotlin/RamlObjectTypesTest.kt index 4f85dc9ef..8f71e330c 100644 --- a/generator/src/test/kotlin/io/outfoxx/sunday/generator/kotlin/RamlObjectTypesTest.kt +++ b/generator/src/test/kotlin/io/outfoxx/sunday/generator/kotlin/RamlObjectTypesTest.kt @@ -16,6 +16,7 @@ package io.outfoxx.sunday.generator.kotlin +import com.github.difflib.text.DiffRowGenerator import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec import io.outfoxx.sunday.generator.GenerationMode @@ -51,10 +52,13 @@ class RamlObjectTypesTest { import kotlin.Any import kotlin.String + import kotlin.collections.List import kotlin.collections.Map public interface Test { public val map: Map + + public val array: List } """.trimIndent(), diff --git a/generator/src/test/kotlin/io/outfoxx/sunday/generator/swift/RamlObjectTypesTest.kt b/generator/src/test/kotlin/io/outfoxx/sunday/generator/swift/RamlObjectTypesTest.kt index 5ff0cff82..9c96566d7 100644 --- a/generator/src/test/kotlin/io/outfoxx/sunday/generator/swift/RamlObjectTypesTest.kt +++ b/generator/src/test/kotlin/io/outfoxx/sunday/generator/swift/RamlObjectTypesTest.kt @@ -51,33 +51,43 @@ class RamlObjectTypesTest { public class Test : Codable, CustomDebugStringConvertible { public var map: [String : Any] + public var array: [Any] public var debugDescription: String { return DescriptionBuilder(Test.self) .add(map, named: "map") + .add(array, named: "array") .build() } - public init(map: [String : Any]) { + public init(map: [String : Any], array: [Any]) { self.map = map + self.array = array } public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.map = try container.decode([String : AnyValue].self, forKey: .map).mapValues { ${'$'}0.unwrapped as Any } + self.map = try container.decode([String : AnyValue].self, forKey: .map).mapValues { ${'$'}0.unwrapped } + self.array = try container.decode([AnyValue].self, forKey: .array).map { ${'$'}0.unwrapped } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.map.mapValues { try AnyValue.wrapped(${'$'}0) }, forKey: .map) + try container.encode(self.array.map { try AnyValue.wrapped(${'$'}0) }, forKey: .array) } public func withMap(map: [String : Any]) -> Test { - return Test(map: map) + return Test(map: map, array: array) + } + + public func withArray(array: [Any]) -> Test { + return Test(map: map, array: array) } fileprivate enum CodingKeys : String, CodingKey { case map = "map" + case array = "array" } @@ -184,8 +194,8 @@ class RamlObjectTypesTest { public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.optionalObject = try container.decodeIfPresent([String : AnyValue].self, forKey: .optionalObject)?.mapValues { ${'$'}0.unwrapped as Any } - self.nillableObject = try container.decodeIfPresent([String : AnyValue].self, forKey: .nillableObject)?.mapValues { ${'$'}0.unwrapped as Any } + self.optionalObject = try container.decodeIfPresent([String : AnyValue].self, forKey: .optionalObject)?.mapValues { ${'$'}0.unwrapped } + self.nillableObject = try container.decodeIfPresent([String : AnyValue].self, forKey: .nillableObject)?.mapValues { ${'$'}0.unwrapped } } public func encode(to encoder: Encoder) throws { diff --git a/generator/src/test/kotlin/io/outfoxx/sunday/generator/swift/SwiftTest.kt b/generator/src/test/kotlin/io/outfoxx/sunday/generator/swift/SwiftTest.kt index 86d48dba3..0bb3067b5 100644 --- a/generator/src/test/kotlin/io/outfoxx/sunday/generator/swift/SwiftTest.kt +++ b/generator/src/test/kotlin/io/outfoxx/sunday/generator/swift/SwiftTest.kt @@ -16,6 +16,7 @@ package io.outfoxx.sunday.generator.swift +import io.outfoxx.sunday.test.extensions.DiffingExtension import io.outfoxx.sunday.test.extensions.ResourceExtension import io.outfoxx.sunday.test.extensions.SwiftCompilerExtension import org.junit.jupiter.api.extension.ExtendWith @@ -25,5 +26,5 @@ import org.junit.jupiter.api.parallel.ResourceLock @Execution(ExecutionMode.CONCURRENT) @ResourceLock("Swift") -@ExtendWith(ResourceExtension::class, SwiftCompilerExtension::class) +@ExtendWith(ResourceExtension::class, SwiftCompilerExtension::class, DiffingExtension::class) annotation class SwiftTest diff --git a/generator/src/test/kotlin/io/outfoxx/sunday/generator/typescript/RamlObjectTypesTest.kt b/generator/src/test/kotlin/io/outfoxx/sunday/generator/typescript/RamlObjectTypesTest.kt index 754f6e99f..39195b7ad 100644 --- a/generator/src/test/kotlin/io/outfoxx/sunday/generator/typescript/RamlObjectTypesTest.kt +++ b/generator/src/test/kotlin/io/outfoxx/sunday/generator/typescript/RamlObjectTypesTest.kt @@ -50,14 +50,19 @@ class RamlObjectTypesTest { map: Record; + array: Array; + } export class Test implements TestSpec { map: Record; + array: Array; + constructor(init: TestSpec) { this.map = init.map; + this.array = init.array; } copy(changes: Partial): Test { @@ -65,7 +70,7 @@ class RamlObjectTypesTest { } toString(): string { - return `Test(map='${'$'}{this.map}')`; + return `Test(map='${'$'}{this.map}', array='${'$'}{this.array}')`; } } diff --git a/generator/src/test/kotlin/io/outfoxx/sunday/generator/typescript/TypeScriptTest.kt b/generator/src/test/kotlin/io/outfoxx/sunday/generator/typescript/TypeScriptTest.kt index dbdab63b5..e0ce46586 100644 --- a/generator/src/test/kotlin/io/outfoxx/sunday/generator/typescript/TypeScriptTest.kt +++ b/generator/src/test/kotlin/io/outfoxx/sunday/generator/typescript/TypeScriptTest.kt @@ -16,6 +16,7 @@ package io.outfoxx.sunday.generator.typescript +import io.outfoxx.sunday.test.extensions.DiffingExtension import io.outfoxx.sunday.test.extensions.ResourceExtension import io.outfoxx.sunday.test.extensions.TypeScriptCompilerExtension import org.junit.jupiter.api.extension.ExtendWith @@ -25,5 +26,5 @@ import org.junit.jupiter.api.parallel.ResourceLock @Execution(CONCURRENT) @ResourceLock("TypeScript") -@ExtendWith(ResourceExtension::class, TypeScriptCompilerExtension::class) +@ExtendWith(ResourceExtension::class, TypeScriptCompilerExtension::class, DiffingExtension::class) annotation class TypeScriptTest diff --git a/generator/src/test/kotlin/io/outfoxx/sunday/test/extensions/DiffingExtension.kt b/generator/src/test/kotlin/io/outfoxx/sunday/test/extensions/DiffingExtension.kt new file mode 100644 index 000000000..881871ee7 --- /dev/null +++ b/generator/src/test/kotlin/io/outfoxx/sunday/test/extensions/DiffingExtension.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2020 Outfox, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.outfoxx.sunday.test.extensions + +import com.github.ajalt.mordant.markdown.Markdown +import com.github.ajalt.mordant.rendering.TextAlign.CENTER +import com.github.ajalt.mordant.rendering.TextColors.* +import com.github.ajalt.mordant.rendering.TextStyle +import com.github.ajalt.mordant.rendering.Whitespace +import com.github.ajalt.mordant.table.Borders +import com.github.ajalt.mordant.table.ColumnWidth +import com.github.ajalt.mordant.table.table +import com.github.ajalt.mordant.terminal.Terminal +import com.github.difflib.text.DiffRow +import com.github.difflib.text.DiffRowGenerator +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.TestExecutionExceptionHandler +import org.opentest4j.AssertionFailedError +import kotlin.math.max + + +class DiffingExtension : TestExecutionExceptionHandler { + + private val differ: DiffRowGenerator = + DiffRowGenerator.create() + .lineNormalizer { it } + .build() + private val terminal = Terminal() + + override fun handleTestExecutionException(context: ExtensionContext, throwable: Throwable) { + if (throwable !is AssertionFailedError || !throwable.isStringMismatch()) { + throw throwable + } + val diff = + differ + .generateDiffRows( + throwable.expected.value.toString().split("\n"), + throwable.actual.value.toString().split("\n") + ) + val maxWidth = diff.maxOf { max(it.oldLine.length, it.newLine.length) } + terminal.println( + table { + style = TextStyle(color = white, bgColor = black, dim = true) + whitespace = Whitespace.PRE + borderStyle = TextStyle(color = white, bgColor = black, dim = true) + + header { + row { + cell("Expected") { align = CENTER } + cell("Actual") { align = CENTER } + } + } + body { + for ((idx, row) in diff.withIndex()) { + val (changeStyle, prefix) = + when (row.tag) { + DiffRow.Tag.INSERT -> TextStyle(color = green) to "+" + DiffRow.Tag.DELETE -> TextStyle(color = red) to row.oldLine.ifBlank { "-" } + DiffRow.Tag.CHANGE -> TextStyle(color = cyan) to "•" + null, DiffRow.Tag.EQUAL -> TextStyle(color = white, dim = true) to "" + } + row { + val borders = if (idx == diff.size - 1) Borders.LEFT_RIGHT_BOTTOM else Borders.LEFT_RIGHT + cell(row.oldLine.padEnd(maxWidth)) { + cellBorders = borders + } + cell(row.newLine.ifEmpty { prefix }.padEnd(maxWidth)) { + style = changeStyle + cellBorders = borders + } + } + } +// row { cells("", "") { cellBorders = Borders.LEFT_RIGHT_BOTTOM } } + } + } + ) + + throw throwable + } + + private fun AssertionFailedError.isStringMismatch(): Boolean = + isExpectedDefined && isActualDefined && + CharSequence::class.java.isAssignableFrom(expected.type) && CharSequence::class.java.isAssignableFrom(actual.type) + +} diff --git a/generator/src/test/resources/raml/type-gen/types/obj-freeform.raml b/generator/src/test/resources/raml/type-gen/types/obj-freeform.raml index e1adbe5b5..49f644fa4 100644 --- a/generator/src/test/resources/raml/type-gen/types/obj-freeform.raml +++ b/generator/src/test/resources/raml/type-gen/types/obj-freeform.raml @@ -9,10 +9,14 @@ types: type: object additionalProperties: true + Array: + type: array + Test: type: object properties: map: Map + array: Array /tests/{id}: diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a6762509d..ea71bca46 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ kotlinLanguage = "1.9" ## amfClient = "5.4.0" clikt = "5.0.1" +diffutils = "4.12" dockerJava = "3.4.0" dokka = "1.9.0" # @pin @@ -33,7 +34,7 @@ shadow = "8.3.3" slf4j = "2.1.0-alpha1" sonarqube = "5.1.0.4882" sundayKt = "1.0.0-beta.24" -swiftPoet = "1.6.5" +swiftPoet = "1.6.6" typeScriptPoet = "1.1.2" validation = "2.0.1.Final" versionCatalogUpdatePlugin = "0.8.5" @@ -60,6 +61,7 @@ versions = { id = "com.github.ben-manes.versions", version.ref = "versions" } amfClient = { module = "com.github.amlorg:amf-api-contract_2.12", version.ref = "amfClient" } clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } cliktMarkdown = { module = "com.github.ajalt.clikt:clikt-markdown", version.ref = "clikt" } +diffutils = { module = "io.github.java-diff-utils:java-diff-utils", version.ref = "diffutils" } dockerJava = { module = "com.github.docker-java:docker-java-core", version.ref = "dockerJava" } dockerJavaTransport = { module = "com.github.docker-java:docker-java-transport-httpclient5", version.ref = "dockerJava" } hamcrest = { module = "org.hamcrest:hamcrest-library", version.ref = "hamcrest" }