From a7a9c73eb2e7bdd47aee7c6dc9752dc4a83e49e3 Mon Sep 17 00:00:00 2001 From: Kevin Wooten Date: Mon, 2 Dec 2024 15:44:40 -0700 Subject: [PATCH 1/2] Update sunday-swift compilation to 1.0.0 --- generator/src/test/resources/swift/compile/local/Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generator/src/test/resources/swift/compile/local/Package.swift b/generator/src/test/resources/swift/compile/local/Package.swift index c900bba41..5143e1738 100644 --- a/generator/src/test/resources/swift/compile/local/Package.swift +++ b/generator/src/test/resources/swift/compile/local/Package.swift @@ -10,7 +10,7 @@ let package = Package( .library(name: "SundayGenTest", targets: ["SundayGenTest"]), ], dependencies: [ - .package(url: "https://github.com/outfoxx/sunday-swift.git", from: "1.0.0-beta.16") + .package(url: "https://github.com/outfoxx/sunday-swift.git", from: "1.0.0") ], targets: [ .target( From 1ca4661134ebdecca9e07466e6d59ae46e7017de Mon Sep 17 00:00:00 2001 From: Kevin Wooten Date: Mon, 2 Dec 2024 15:45:58 -0700 Subject: [PATCH 2/2] More fixes for inheritance with patchable types --- .../generator/kotlin/KotlinTypeRegistry.kt | 2 +- .../generator/swift/SwiftTypeRegistry.kt | 19 +- .../kotlin/RamlTypeAnnotationsTest.kt | 120 ++++++++++- .../swift/RamlTypeAnnotationsTest.kt | 192 ++++++++++++++++++ .../typescript/RamlTypeAnnotationsTest.kt | 46 +++++ .../annotations/type-patchable-disc.raml | 34 ++++ 6 files changed, 403 insertions(+), 10 deletions(-) create mode 100644 generator/src/test/resources/raml/type-gen/annotations/type-patchable-disc.raml 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 734f05fa9..883e7c962 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 @@ -929,7 +929,7 @@ class KotlinTypeRegistry( } } - if (isPatchable && options.contains(ImplementModel)) { + if (isPatchable && options.contains(ImplementModel) && !typeBuilder.modifiers.contains(KModifier.ABSTRACT)) { val initLambdaTypeName = LambdaTypeName.get(className, listOf(), UNIT) 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 6f6d8294d..298c7a181 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 @@ -28,6 +28,7 @@ import io.outfoxx.sunday.generator.APIAnnotationName.* import io.outfoxx.sunday.generator.common.DefinitionLocation import io.outfoxx.sunday.generator.common.GenerationHeaders import io.outfoxx.sunday.generator.common.ShapeIndex +import io.outfoxx.sunday.generator.kotlin.utils.kotlinIdentifierName import io.outfoxx.sunday.generator.swift.SwiftTypeRegistry.Option.AddGeneratedHeader import io.outfoxx.sunday.generator.swift.utils.* import io.outfoxx.sunday.generator.utils.* @@ -1274,8 +1275,16 @@ class SwiftTypeRegistry( if (isPatchable) { - val typeSpec = typeBuilder.build() - val propertySpecs = typeSpec.propertySpecs.filter { it.name != DESCRIPTION_PROP_NAME } + val properties = + (inheritedDeclaredProperties + localDeclaredProperties) + .associate { property -> + val propertyTypeName = resolvePropertyTypeName(property, className, context) + .run { + val base = if (property.nullable) PATCH_OP else UPDATE_OP + base.parameterizedBy(makeNonOptional()).makeOptional() + } + property.swiftIdentifierName to propertyTypeName + } val patchOpExt = ExtensionSpec.builder(ANY_PATCH_OP) @@ -1285,9 +1294,9 @@ class SwiftTypeRegistry( .addModifiers(PUBLIC, STATIC) .returns(SelfTypeName.INSTANCE) .apply { - for (propertySpec in propertySpecs) { + for ((propertyName, propertyTypeName) in properties) { addParameter( - ParameterSpec.builder(propertySpec.name, propertySpec.type) + ParameterSpec.builder(propertyName, propertyTypeName) .defaultValue(".none") .build(), ) @@ -1297,7 +1306,7 @@ class SwiftTypeRegistry( "%T.merge(%T(%L))", SelfTypeName.INSTANCE, className, - propertySpecs.map { CodeBlock.of("%L: %L", it.name, it.name) }.joinToCode(",%W"), + properties.keys.map { CodeBlock.of("%L: %L", it, it) }.joinToCode(",%W"), ) .build(), ) diff --git a/generator/src/test/kotlin/io/outfoxx/sunday/generator/kotlin/RamlTypeAnnotationsTest.kt b/generator/src/test/kotlin/io/outfoxx/sunday/generator/kotlin/RamlTypeAnnotationsTest.kt index 6dc7a6acc..0912ae8dc 100644 --- a/generator/src/test/kotlin/io/outfoxx/sunday/generator/kotlin/RamlTypeAnnotationsTest.kt +++ b/generator/src/test/kotlin/io/outfoxx/sunday/generator/kotlin/RamlTypeAnnotationsTest.kt @@ -28,10 +28,7 @@ import io.outfoxx.sunday.generator.kotlin.KotlinTypeRegistry.Option.JacksonAnnot import io.outfoxx.sunday.generator.kotlin.tools.findType import io.outfoxx.sunday.generator.kotlin.tools.generateTypes import io.outfoxx.sunday.test.extensions.ResourceUri -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -1054,6 +1051,121 @@ class RamlTypeAnnotationsTest { ) } + @Test + fun `test discriminated patchable class generation`( + @ResourceUri("raml/type-gen/annotations/type-patchable-disc.raml") testUri: URI, + ) { + + val typeRegistry = KotlinTypeRegistry("io.test", null, Server, setOf(ImplementModel, JacksonAnnotations)) + + val generatedTypes = generateTypes(testUri, typeRegistry) + val typeSpec = findType("io.test.Test", generatedTypes) + + assertEquals( + """ + package io.test + + import com.fasterxml.jackson.`annotation`.JsonInclude + import com.fasterxml.jackson.`annotation`.JsonSubTypes + import com.fasterxml.jackson.`annotation`.JsonTypeInfo + import io.outfoxx.sunday.json.patch.Patch + import kotlin.Any + import kotlin.Boolean + import kotlin.String + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + property = "type", + ) + @JsonSubTypes(value = [ + JsonSubTypes.Type(value = Child::class) + ]) + public abstract class Test() : Patch { + public abstract val type: TestType + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + return true + } + + override fun toString(): String = ""${'"'}Test()""${'"'} + } + + """.trimIndent(), + buildString { + FileSpec.get("io.test", typeSpec) + .writeTo(this) + }, + ) + + val typeSpec2 = findType("io.test.Child", generatedTypes) + + assertEquals( + """ + package io.test + + import com.fasterxml.jackson.`annotation`.JsonInclude + import com.fasterxml.jackson.`annotation`.JsonTypeName + import io.outfoxx.sunday.json.patch.Patch + import io.outfoxx.sunday.json.patch.PatchOp + import io.outfoxx.sunday.json.patch.UpdateOp + import kotlin.Any + import kotlin.Boolean + import kotlin.Int + import kotlin.String + import kotlin.Unit + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + @JsonTypeName("child") + public class Child( + public var child: UpdateOp = PatchOp.none(), + ) : Test(), + Patch { + override val type: TestType + get() = TestType.Child + + override fun hashCode(): Int { + var result = 31 * super.hashCode() + result = 31 * result + child.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Child + + if (child != other.child) return false + + return true + } + + override fun toString(): String = ""${'"'}Child(child='${'$'}child')""${'"'} + + public companion object { + public inline fun merge(`init`: Child.() -> Unit): PatchOp.Set { + val patch = Child() + patch.init() + return PatchOp.Set(patch) + } + + public inline fun patch(`init`: Child.() -> Unit): Child = merge(init).value + } + } + + """.trimIndent(), + buildString { + FileSpec.get("io.test", typeSpec2) + .writeTo(this) + }, + ) + } + @Test fun `test types can be generated in one mode and overridden in another`( @ResourceUri("raml/type-gen/annotations/type-kotlin-type-dual.raml") testUri: URI, diff --git a/generator/src/test/kotlin/io/outfoxx/sunday/generator/swift/RamlTypeAnnotationsTest.kt b/generator/src/test/kotlin/io/outfoxx/sunday/generator/swift/RamlTypeAnnotationsTest.kt index a661d3c30..87e5cf588 100644 --- a/generator/src/test/kotlin/io/outfoxx/sunday/generator/swift/RamlTypeAnnotationsTest.kt +++ b/generator/src/test/kotlin/io/outfoxx/sunday/generator/swift/RamlTypeAnnotationsTest.kt @@ -1404,4 +1404,196 @@ class RamlTypeAnnotationsTest { }, ) } + + @Test + fun `test discriminated patchable class generation`( + compiler: SwiftCompiler, + @ResourceUri("raml/type-gen/annotations/type-patchable-disc.raml") testUri: URI, + ) { + + val typeRegistry = SwiftTypeRegistry(setOf()) + + val generatedTypes = generateTypes(testUri, typeRegistry, compiler) + val testTypeTypeSpec = findType("TestType", generatedTypes) + val testTypeSpec = findType("Test", generatedTypes) + val childTypeSpec = findType("Child", generatedTypes) + + assertEquals( + """ + public enum TestType : String, CaseIterable, Codable { + + case child = "child" + + } + + """.trimIndent(), + buildString { + FileSpec.builder("", testTypeTypeSpec.name) + .addType(testTypeTypeSpec) + .apply { + testTypeTypeSpec.tag()?.forEach { addExtension(it) } + } + .build() + .writeTo(this) + }, + ) + assertEquals( + """ + import Sunday + + public class Test : Codable, CustomDebugStringConvertible { + + public var type: TestType { + fatalError("abstract type method") + } + public var debugDescription: String { + return DescriptionBuilder(Test.self) + .build() + } + + public init() { + } + + public required init(from decoder: Decoder) throws { + let _ = try decoder.container(keyedBy: CodingKeys.self) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.type, forKey: .type) + } + + public enum AnyRef : Codable, CustomDebugStringConvertible { + + case child(Child) + + public var value: Test { + switch self { + case .child(let value): return value + } + } + public var debugDescription: String { + switch self { + case .child(let value): return value.debugDescription + } + } + + public init(value: Test) { + switch value { + case let value as Child: self = .child(value) + default: fatalError("Invalid value type") + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(TestType.self, forKey: CodingKeys.type) + switch type { + case .child: self = .child(try Child(from: decoder)) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .child(let value): try container.encode(value) + } + } + + } + + fileprivate enum CodingKeys : String, CodingKey { + + case type = "type" + + } + + } + + extension AnyPatchOp where Value == Test { + + public static func merge() -> Self { + Self.merge(Test()) + } + + } + + """.trimIndent(), + buildString { + FileSpec.builder("", testTypeSpec.name) + .addType(testTypeSpec) + .apply { + testTypeSpec.tag()?.forEach { addExtension(it) } + } + .build() + .writeTo(this) + }, + ) + + assertEquals( + """ + import Sunday + + public class Child : Test { + + public override var type: TestType { + return TestType.child + } + public var child: UpdateOp? + public override var debugDescription: String { + return DescriptionBuilder(Child.self) + .add(type, named: "type") + .add(child, named: "child") + .build() + } + + public init(child: UpdateOp? = .none) { + self.child = child + super.init() + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.child = try container.decodeIfExists(String.self, forKey: .child) + try super.init(from: decoder) + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfExists(self.child, forKey: .child) + } + + public func withChild(child: UpdateOp?) -> Child { + return Child(child: child) + } + + fileprivate enum CodingKeys : String, CodingKey { + + case child = "child" + + } + + } + + extension AnyPatchOp where Value == Child { + + public static func merge(child: UpdateOp? = .none) -> Self { + Self.merge(Child(child: child)) + } + + } + + """.trimIndent(), + buildString { + FileSpec.builder("", childTypeSpec.name) + .addType(childTypeSpec) + .apply { + childTypeSpec.tag()?.forEach { addExtension(it) } + } + .build() + .writeTo(this) + }, + ) + } } diff --git a/generator/src/test/kotlin/io/outfoxx/sunday/generator/typescript/RamlTypeAnnotationsTest.kt b/generator/src/test/kotlin/io/outfoxx/sunday/generator/typescript/RamlTypeAnnotationsTest.kt index f41c51d9c..bdf2ccba9 100644 --- a/generator/src/test/kotlin/io/outfoxx/sunday/generator/typescript/RamlTypeAnnotationsTest.kt +++ b/generator/src/test/kotlin/io/outfoxx/sunday/generator/typescript/RamlTypeAnnotationsTest.kt @@ -1184,4 +1184,50 @@ class RamlTypeAnnotationsTest { }, ) } + + @Test + fun `test discriminated patchable class generation with Jackson`( + compiler: TypeScriptCompiler, + @ResourceUri("raml/type-gen/annotations/type-patchable-disc.raml") testUri: URI, + ) { + + val typeRegistry = TypeScriptTypeRegistry(setOf(JacksonDecorators)) + + val typeSpec = findTypeMod("Test@!test", generateTypes(testUri, typeRegistry, compiler, includeIndex = true)) + + assertEquals( + """ + import {Child} from './index'; + import {JsonInclude, JsonIncludeType, JsonSubTypes, JsonTypeInfo, JsonTypeInfoAs, JsonTypeInfoId} from '@outfoxx/jackson-js'; + + + export interface TestSpec { + } + + @JsonInclude({value: JsonIncludeType.ALWAYS}) + @JsonTypeInfo({ + use: JsonTypeInfoId.NAME, + include: JsonTypeInfoAs.PROPERTY, + property: 'type', + }) + @JsonSubTypes({ + types: [ + {class: () => Child, name: 'child' /* TestType.Child */} + ] + }) + export abstract class Test implements TestSpec { + + toString(): string { + return `Test()`; + } + + } + + """.trimIndent(), + buildString { + FileSpec.get(typeSpec) + .writeTo(this) + }, + ) + } } diff --git a/generator/src/test/resources/raml/type-gen/annotations/type-patchable-disc.raml b/generator/src/test/resources/raml/type-gen/annotations/type-patchable-disc.raml new file mode 100644 index 000000000..ce599ebf0 --- /dev/null +++ b/generator/src/test/resources/raml/type-gen/annotations/type-patchable-disc.raml @@ -0,0 +1,34 @@ +#%RAML 1.0 +title: Test API +uses: + sunday: https://outfoxx.github.io/sunday-generator/sunday.raml +mediaType: +- application/json + +types: + + TestType: + type: string + enum: [ child ] + + Test: + type: object + (sunday.patchable): true + discriminator: type + properties: + type: TestType + + Child: + type: Test + discriminatorValue: child + properties: + child: string + +/tests/{id}: + + get: + displayName: fetchTest + responses: + 200: + body: + type: Test