From 03d6f9a940dfe606032f4d4c087f0b92f54f87ff Mon Sep 17 00:00:00 2001 From: Adam <897017+aSemy@users.noreply.github.com> Date: Sun, 10 Apr 2022 23:21:47 +0200 Subject: [PATCH] #26 named tuples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - implement labelled tuple elements, add tests, fix existing - simplify 'TSProperty', it only needs to be one class - TSCompile tests passed ✅ --- docs/code/example/example-tuple-02.kt | 38 ++++---- docs/code/example/example-tuple-03.kt | 36 +++---- docs/code/example/example-tuple-04.kt | 33 +++++-- docs/code/example/example-tuple-05.kt | 21 +++++ docs/code/test/TuplesTest.kt | 51 +++++++++- docs/tuples.md | 93 +++++++++++++++++-- .../kxstsgen/core/KxsTsSourceCodeGenerator.kt | 15 +-- .../kxstsgen/core/TsElementConverter.kt | 27 +++--- .../adamko/kxstsgen/core/experiments/tuple.kt | 15 ++- .../dev/adamko/kxstsgen/core/tsElements.kt | 26 ++---- 10 files changed, 257 insertions(+), 98 deletions(-) create mode 100644 docs/code/example/example-tuple-05.kt diff --git a/docs/code/example/example-tuple-02.kt b/docs/code/example/example-tuple-02.kt index 8dac896d..07282f63 100644 --- a/docs/code/example/example-tuple-02.kt +++ b/docs/code/example/example-tuple-02.kt @@ -6,29 +6,27 @@ import dev.adamko.kxstsgen.* import dev.adamko.kxstsgen.core.experiments.TupleSerializer import kotlinx.serialization.* -@Serializable(with = OptionalFields.Serializer::class) -data class OptionalFields( - val requiredString: String, - val optionalString: String = "", - val nullableString: String?, - val nullableOptionalString: String? = "", +@Serializable(with = PostalAddressUSA.Serializer::class) +data class PostalAddressUSA( + @SerialName("num") // 'SerialName' will be ignored in 'Tuple' form + val houseNumber: String, + val streetName: String, + val postcode: String, ) { - object Serializer : TupleSerializer( - "OptionalFields", + object Serializer : TupleSerializer( + "PostalAddressUSA", { - element(OptionalFields::requiredString) - element(OptionalFields::optionalString) - element(OptionalFields::nullableString) - element(OptionalFields::nullableOptionalString) + element(PostalAddressUSA::houseNumber) + // custom labels for 'streetName', 'postcode' + element("street", PostalAddressUSA::streetName) + element("zip", PostalAddressUSA::postcode) } ) { - override fun tupleConstructor(elements: Iterator<*>): OptionalFields { - val iter = elements.iterator() - return OptionalFields( - iter.next() as String, - iter.next() as String, - iter.next() as String, - iter.next() as String, + override fun tupleConstructor(elements: Iterator<*>): PostalAddressUSA { + return PostalAddressUSA( + elements.next() as String, + elements.next() as String, + elements.next() as String, ) } } @@ -36,5 +34,5 @@ data class OptionalFields( fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(OptionalFields.serializer())) + println(tsGenerator.generate(PostalAddressUSA.serializer())) } diff --git a/docs/code/example/example-tuple-03.kt b/docs/code/example/example-tuple-03.kt index 6838945d..4fa0fb11 100644 --- a/docs/code/example/example-tuple-03.kt +++ b/docs/code/example/example-tuple-03.kt @@ -6,25 +6,29 @@ import dev.adamko.kxstsgen.* import dev.adamko.kxstsgen.core.experiments.TupleSerializer import kotlinx.serialization.* -@Serializable(with = Coordinates.Serializer::class) -data class Coordinates( - val x: Int, - val y: Int, - val z: Int, +@Serializable(with = OptionalFields.Serializer::class) +data class OptionalFields( + val requiredString: String, + val optionalString: String = "", + val nullableString: String?, + val nullableOptionalString: String? = "", ) { - object Serializer : TupleSerializer( - "Coordinates", + object Serializer : TupleSerializer( + "OptionalFields", { - element(Coordinates::x) - element(Coordinates::y) - element(Coordinates::z) + element(OptionalFields::requiredString) + element(OptionalFields::optionalString) + element(OptionalFields::nullableString) + element(OptionalFields::nullableOptionalString) } ) { - override fun tupleConstructor(elements: Iterator<*>): Coordinates { - return Coordinates( - elements.next() as Int, - elements.next() as Int, - elements.next() as Int, + override fun tupleConstructor(elements: Iterator<*>): OptionalFields { + val iter = elements.iterator() + return OptionalFields( + iter.next() as String, + iter.next() as String, + iter.next() as String, + iter.next() as String, ) } } @@ -32,5 +36,5 @@ data class Coordinates( fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(Coordinates.serializer())) + println(tsGenerator.generate(OptionalFields.serializer())) } diff --git a/docs/code/example/example-tuple-04.kt b/docs/code/example/example-tuple-04.kt index bae81b03..a8026af9 100644 --- a/docs/code/example/example-tuple-04.kt +++ b/docs/code/example/example-tuple-04.kt @@ -6,16 +6,31 @@ import dev.adamko.kxstsgen.* import dev.adamko.kxstsgen.core.experiments.TupleSerializer import kotlinx.serialization.* -import dev.adamko.kxstsgen.example.exampleTuple03.Coordinates - -@Serializable -class GameLocations( - val homeLocation: Coordinates, - val allLocations: List, - val namedLocations: Map, -) +@Serializable(with = Coordinates.Serializer::class) +data class Coordinates( + val x: Int, + val y: Int, + val z: Int, +) { + object Serializer : TupleSerializer( + "Coordinates", + { + element(Coordinates::x) + element(Coordinates::y) + element(Coordinates::z) + } + ) { + override fun tupleConstructor(elements: Iterator<*>): Coordinates { + return Coordinates( + elements.next() as Int, + elements.next() as Int, + elements.next() as Int, + ) + } + } +} fun main() { val tsGenerator = KxsTsGenerator() - println(tsGenerator.generate(GameLocations.serializer())) + println(tsGenerator.generate(Coordinates.serializer())) } diff --git a/docs/code/example/example-tuple-05.kt b/docs/code/example/example-tuple-05.kt new file mode 100644 index 00000000..87b33f36 --- /dev/null +++ b/docs/code/example/example-tuple-05.kt @@ -0,0 +1,21 @@ +// This file was automatically generated from tuples.md by Knit tool. Do not edit. +@file:Suppress("PackageDirectoryMismatch", "unused") +package dev.adamko.kxstsgen.example.exampleTuple05 + +import dev.adamko.kxstsgen.* +import dev.adamko.kxstsgen.core.experiments.TupleSerializer +import kotlinx.serialization.* + +import dev.adamko.kxstsgen.example.exampleTuple04.Coordinates + +@Serializable +class GameLocations( + val homeLocation: Coordinates, + val allLocations: List, + val namedLocations: Map, +) + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(GameLocations.serializer())) +} diff --git a/docs/code/test/TuplesTest.kt b/docs/code/test/TuplesTest.kt index 3d1310ed..cdb87d3b 100644 --- a/docs/code/test/TuplesTest.kt +++ b/docs/code/test/TuplesTest.kt @@ -20,7 +20,13 @@ class TuplesTest : FunSpec({ actual.shouldBe( // language=TypeScript """ - |export type SimpleTypes = [string, number, number | null, boolean, string]; + |export type SimpleTypes = [ + | aString: string, + | anInt: number, + | aDouble: number | null, + | bool: boolean, + | privateMember: string, + |]; """.trimMargin() .normalize() ) @@ -40,7 +46,11 @@ class TuplesTest : FunSpec({ actual.shouldBe( // language=TypeScript """ - |export type OptionalFields = [string, string, string | null, string | null]; + |export type PostalAddressUSA = [ + | houseNumber: string, // @SerialName("num") was ignored + | street: string, // custom name + | zip: string, // custom name + |]; """.trimMargin() .normalize() ) @@ -60,7 +70,12 @@ class TuplesTest : FunSpec({ actual.shouldBe( // language=TypeScript """ - |export type Coordinates = [number, number, number]; + |export type OptionalFields = [ + | requiredString: string, + | optionalString: string, + | nullableString: string | null, + | nullableOptionalString: string | null, + |]; """.trimMargin() .normalize() ) @@ -76,6 +91,30 @@ class TuplesTest : FunSpec({ dev.adamko.kxstsgen.example.exampleTuple04.main() }.normalizeJoin() + test("expect actual matches TypeScript") { + actual.shouldBe( + // language=TypeScript + """ + |export type Coordinates = [ + | x: number, + | y: number, + | z: number, + |]; + """.trimMargin() + .normalize() + ) + } + + test("expect actual compiles").config(tags = tsCompile) { + actual.shouldTypeScriptCompile() + } + } + + context("ExampleTuple05") { + val actual = captureOutput("ExampleTuple05") { + dev.adamko.kxstsgen.example.exampleTuple05.main() + }.normalizeJoin() + test("expect actual matches TypeScript") { actual.shouldBe( // language=TypeScript @@ -86,7 +125,11 @@ class TuplesTest : FunSpec({ | namedLocations: { [key: string]: Coordinates }; |} | - |export type Coordinates = [number, number, number]; + |export type Coordinates = [ + | x: number, + | y: number, + | z: number, + |]; """.trimMargin() .normalize() ) diff --git a/docs/tuples.md b/docs/tuples.md index 306ccba2..10cd274d 100644 --- a/docs/tuples.md +++ b/docs/tuples.md @@ -7,9 +7,10 @@ * [Tuples](#tuples) * [Tuple example](#tuple-example) + * [Tuple labels](#tuple-labels) * [Optional elements in tuples](#optional-elements-in-tuples) * [Properties all the same type](#properties-all-the-same-type) - * [Tuples as interface properties](#tuples-as-interface-properties) + * [Tuples as interface properties](#tuples-as-interface-properties) @@ -80,7 +81,66 @@ fun main() { > You can get the full code [here](./code/example/example-tuple-01.kt). ```typescript -export type SimpleTypes = [string, number, number | null, boolean, string]; +export type SimpleTypes = [ + aString: string, + anInt: number, + aDouble: number | null, + bool: boolean, + privateMember: string, +]; +``` + + + +### Tuple labels + +By default, the tuple elements are labelled with the names of properties, not the `@SerialName`, +which will be ignored. This isn't important for serialization because the tuple will be serialized +without the name of the property. + +The name of the label can be overridden if desired while defining the elements. + +```kotlin +@Serializable(with = PostalAddressUSA.Serializer::class) +data class PostalAddressUSA( + @SerialName("num") // 'SerialName' will be ignored in 'Tuple' form + val houseNumber: String, + val streetName: String, + val postcode: String, +) { + object Serializer : TupleSerializer( + "PostalAddressUSA", + { + element(PostalAddressUSA::houseNumber) + // custom labels for 'streetName', 'postcode' + element("street", PostalAddressUSA::streetName) + element("zip", PostalAddressUSA::postcode) + } + ) { + override fun tupleConstructor(elements: Iterator<*>): PostalAddressUSA { + return PostalAddressUSA( + elements.next() as String, + elements.next() as String, + elements.next() as String, + ) + } + } +} + +fun main() { + val tsGenerator = KxsTsGenerator() + println(tsGenerator.generate(PostalAddressUSA.serializer())) +} +``` + +> You can get the full code [here](./code/example/example-tuple-02.kt). + +```typescript +export type PostalAddressUSA = [ + houseNumber: string, // @SerialName("num") was ignored + street: string, // custom name + zip: string, // custom name +]; ``` @@ -129,10 +189,15 @@ fun main() { } ``` -> You can get the full code [here](./code/example/example-tuple-02.kt). +> You can get the full code [here](./code/example/example-tuple-03.kt). ```typescript -export type OptionalFields = [string, string, string | null, string | null]; +export type OptionalFields = [ + requiredString: string, + optionalString: string, + nullableString: string | null, + nullableOptionalString: string | null, +]; ``` @@ -170,18 +235,22 @@ fun main() { } ``` -> You can get the full code [here](./code/example/example-tuple-03.kt). +> You can get the full code [here](./code/example/example-tuple-04.kt). ```typescript -export type Coordinates = [number, number, number]; +export type Coordinates = [ + x: number, + y: number, + z: number, +]; ``` -#### Tuples as interface properties +### Tuples as interface properties ```kotlin -import dev.adamko.kxstsgen.example.exampleTuple03.Coordinates +import dev.adamko.kxstsgen.example.exampleTuple04.Coordinates @Serializable class GameLocations( @@ -196,7 +265,7 @@ fun main() { } ``` -> You can get the full code [here](./code/example/example-tuple-04.kt). +> You can get the full code [here](./code/example/example-tuple-05.kt). ```typescript export interface GameLocations { @@ -205,7 +274,11 @@ export interface GameLocations { namedLocations: { [key: string]: Coordinates }; } -export type Coordinates = [number, number, number]; +export type Coordinates = [ + x: number, + y: number, + z: number, +]; ``` diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/KxsTsSourceCodeGenerator.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/KxsTsSourceCodeGenerator.kt index 9a57a52a..cbcc2b89 100644 --- a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/KxsTsSourceCodeGenerator.kt +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/KxsTsSourceCodeGenerator.kt @@ -111,7 +111,7 @@ abstract class KxsTsSourceCodeGenerator( * ``` */ open fun generateInterfaceProperty( - property: TsProperty.Named + property: TsProperty ): String { val separator = if (property.optional) "?: " else ": " val propertyType = generateTypeReference(property.typeRef) @@ -166,14 +166,17 @@ abstract class KxsTsSourceCodeGenerator( override fun generateTuple(tuple: TsDeclaration.TsTuple): String { - val types = tuple.elements.joinToString(separator = ", ") { - val optionalMarker = if (it.optional) "?" else "" - generateTypeReference(it.typeRef) + optionalMarker + val types = tuple.elements.joinToString(separator = "\n") { property -> + val optionalMarker = if (property.optional) "?" else "" + val typeRef = generateTypeReference(property.typeRef) + "${config.indent}${property.name}: $typeRef$optionalMarker," } return """ - |export type ${tuple.id.name} = [$types]; - """.trimMargin() + ¦export type ${tuple.id.name} = [ + ¦$types + ¦]; + """.trimMargin("¦") } diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/TsElementConverter.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/TsElementConverter.kt index a8dece17..ee978554 100644 --- a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/TsElementConverter.kt +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/TsElementConverter.kt @@ -126,7 +126,7 @@ fun interface TsElementConverter { false, ) - val literalTypeProperty = TsProperty.Named(discriminatorName, false, literalTypeRef) + val literalTypeProperty = TsProperty(discriminatorName, literalTypeRef, false) subclass.copy(properties = setOf(literalTypeProperty) + subclass.properties) } @@ -165,12 +165,7 @@ fun interface TsElementConverter { ): TsDeclaration { val resultId = elementIdConverter(descriptor) - val properties = descriptor.elementDescriptors.mapIndexed { index, fieldDescriptor -> - val name = descriptor.getElementName(index) - val fieldTypeRef = typeRefConverter(fieldDescriptor) - val optional = descriptor.isElementOptional(index) - TsProperty.Named(name, optional, fieldTypeRef) - }.toSet() + val properties = convertProperties(descriptor) return TsDeclaration.TsInterface(resultId, properties) } @@ -181,16 +176,24 @@ fun interface TsElementConverter { ): TsDeclaration.TsTuple { val resultId = elementIdConverter(descriptor) - val properties = descriptor.elementDescriptors.mapIndexed { index, fieldDescriptor -> - val fieldTypeRef = typeRefConverter(fieldDescriptor) - val optional = descriptor.isElementOptional(index) - TsProperty.Unnamed(optional, fieldTypeRef) - } + val properties = convertProperties(descriptor) return TsDeclaration.TsTuple(resultId, properties) } + open fun convertProperties( + descriptor: SerialDescriptor, + ): Set { + return descriptor.elementDescriptors.mapIndexed { index, fieldDescriptor -> + val name = descriptor.getElementName(index) + val fieldTypeRef = typeRefConverter(fieldDescriptor) + val optional = descriptor.isElementOptional(index) + TsProperty(name, fieldTypeRef, optional) + }.toSet() + } + + open fun convertEnum( enumDescriptor: SerialDescriptor, ): TsDeclaration.TsEnum { diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/experiments/tuple.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/experiments/tuple.kt index 1423442a..ee8bc98f 100644 --- a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/experiments/tuple.kt +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/experiments/tuple.kt @@ -2,6 +2,7 @@ package dev.adamko.kxstsgen.core.experiments +import kotlin.reflect.KProperty1 import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.SerialDescriptor @@ -17,6 +18,7 @@ import kotlinx.serialization.serializer open class TupleElement( + open val name: String, open val index: Int, open val elementSerializer: KSerializer, open val elementAccessor: T.() -> E, @@ -45,10 +47,12 @@ open class TupleElement( inline fun tupleElement( index: Int, + name: String, noinline elementAccessor: T.() -> E, serializer: KSerializer = serializer(), ): TupleElement { return TupleElement( + name, index, serializer, elementAccessor, @@ -73,9 +77,16 @@ class TupleElementsBuilder { val elementsSize by _elements::size inline fun element( + property: KProperty1 + ) { + element(property.name, property) + } + + inline fun element( + name: String, noinline elementAccessor: T.() -> E, ) { - element(tupleElement(elementsSize, elementAccessor)) + element(tupleElement(elementsSize, name, elementAccessor)) } fun element(element: TupleElement) { @@ -106,7 +117,7 @@ abstract class TupleSerializer( tupleElements .sortedBy { it.index } .forEach { tupleElement -> - element("element${tupleElement.index}", tupleElement.descriptor) + element(tupleElement.name, tupleElement.descriptor) } } diff --git a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/tsElements.kt b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/tsElements.kt index 8961ded8..45011488 100644 --- a/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/tsElements.kt +++ b/modules/kxs-ts-gen-core/src/commonMain/kotlin/dev/adamko/kxstsgen/core/tsElements.kt @@ -55,13 +55,13 @@ sealed interface TsDeclaration : TsElement { /** A [tuple type](https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types). */ data class TsTuple( override val id: TsElementId, - val elements: List, + val elements: Set, ) : TsDeclaration data class TsInterface( override val id: TsElementId, - val properties: Set, + val properties: Set, ) : TsDeclaration @@ -158,26 +158,14 @@ sealed interface TsTypeRef { * A property within an [interface][TsDeclaration.TsInterface] * or [tuple][TsDeclaration.TsTuple]. */ -sealed interface TsProperty { - val typeRef: TsTypeRef +data class TsProperty( + val name: String, + val typeRef: TsTypeRef, /** * A property may be required or optional. See the TypeScript docs: * ['Optional Properties'](https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties) * * Optionality is different to nullability, which is defined by [TsTypeRef.nullable]. */ - val optional: Boolean - - - data class Named( - val name: String, - override val optional: Boolean, - override val typeRef: TsTypeRef, - ) : TsProperty - - - data class Unnamed( - override val optional: Boolean, - override val typeRef: TsTypeRef, - ) : TsProperty -} + val optional: Boolean, +)