asyncapi / modelina

A library for generating typed models based on inputs such as AsyncAPI, OpenAPI, and JSON Schema documents with high customization
https://modelina.org
Apache License 2.0
306 stars 178 forks source link

Render Unions as custom type in Kotlin Generator #1127

Open LouisXhaferi opened 1 year ago

LouisXhaferi commented 1 year ago

Reason/Context

Since JVM languages don't support type unions, the Kotlin generator currently renders unions as Any, which is Kotlin's supertype from which all classes inherit. (The Java Generator also does this, but with Object, See https://github.com/asyncapi/modelina/issues/749)

Representing unions as Any means that the user of the generated code would have to try to cast or handle the value of the respective field, each time that he uses it, which is highly inconvenient.

Therefore we should introduce a feature that renders union as a custom type actually describing possible value types, that allows the user to leverage language features to just use the field.

Description

The changes will most likely all be contained in the domain of the Kotlin generator. They require a breaking change

An optimal solution would be to implement the custom type as a sealed interface nested in the generated model.

A type union of an integer and a string could be portrayed as

data class Model(
    val value: StringIntegerUnion
) { 
  sealed interface StringIntegerUnion {
    class StringType(val value: String) : StringIntegerUnion
    class IntType(val value: Int): StringIntegerUnion
  }
}

This would then enable the user to use language features like the when condition paired with is as well as smart casts. An example of how that would be.

// imports omitted for brevity

fun main() {
    val unionString: StringIntegerUnion = StringIntegerUnion.StringType("bla bla")
    val unionInt: StringIntegerUnion = StringIntegerUnion.IntType(123)

    printType(unionString)
    printType(unionInt)
}

fun printType(union: StringIntegerUnion) {
    when(union) {
        is StringType -> println("is string")
        is IntType -> println("is int")
        else -> println("is something else")
    }
}

But implement how ever you like

Scope:

LouisXhaferi commented 1 year ago

Also one semi important note: Kotlin does not support a null type, schema languages often do. Come up with a way to handle it. Some examples type = ['string', 'null'] or type = ['string', 'integer', 'null']. How do you handle these?

Some options how you could handle it:

Option 1: Nullable* sealed interface, with nullable values in subclass constructorrs.

  sealed interface NullableStringIntegerUnion {
    class StringType(val value: String?) : StringIntegerUnion
    class IntType(val value: Int?): StringIntegerUnion
  }

Option 2: Extra NullType

  sealed interface NullableStringIntegerUnion {
    class StringType(val value: String) : NullableStringIntegerUnion
    class IntType(val value: Int): NullableStringIntegerUnion
    class NullType : NullableStringIntegerUnion
  }

Both of these are kind of a nuisance and force the user to do custom json mapping (most likely).

But there are probably wayy better ways. 😉

LouisXhaferi commented 1 year ago

I'm gonna start implementing this this weekend. Wish me luck. (jk)

jonaslagoni commented 1 year ago

@LouisXhaferi i would forget about null for now, and just focus on the simple union, because currently null is "forgotten" i.e. you dont have access to it anywhere at the moment 😅

LouisXhaferi commented 1 year ago

Yeah okay I'm not gonna get it done and don't have enough time to make serious progress.

I want this to work without a preset and while I still get the TypeMapping to spit out the correct name, I'm struggling to get the unions model to be inside the model that references it.

jonaslagoni commented 1 year ago

Maybe I can give you some pointers, the way I would achieve it:

That should more or less do it I think 🤔

LouisXhaferi commented 1 year ago

Thanks for the tips, I had those things done, apart from splitObject: true. One thing I set out to, was to have the Unions inside of the actual models.

Let's see if I get that done.

jonaslagoni commented 1 year ago

That's gonna be really tough, and not very extensible 😅 Generally the way it's setup is that each model rendering has no direct interaction with the others.

Any specific reason you want to inline the unions? 🤔

github-actions[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity :sleeping:

It will be closed in 120 days if no further activity occurs. To unstale this issue, add a comment with a detailed explanation.

There can be many reasons why some specific issue has no activity. The most probable cause is lack of time, not lack of interest. AsyncAPI Initiative is a Linux Foundation project not owned by a single for-profit company. It is a community-driven initiative ruled under open governance model.

Let us figure out together how to push this issue forward. Connect with us through one of many communication channels we established here.

Thank you for your patience :heart:

AnimeshKumar923 commented 9 months ago

@jonaslagoni still valid?

jonaslagoni commented 9 months ago

Yep still valid.

jonaslagoni commented 9 months ago

Here is the way to achieve it: https://github.com/asyncapi/modelina/issues/1127#issuecomment-1507534472

github-actions[bot] commented 5 months ago

This issue has been automatically marked as stale because it has not had recent activity :sleeping:

It will be closed in 120 days if no further activity occurs. To unstale this issue, add a comment with a detailed explanation.

There can be many reasons why some specific issue has no activity. The most probable cause is lack of time, not lack of interest. AsyncAPI Initiative is a Linux Foundation project not owned by a single for-profit company. It is a community-driven initiative ruled under open governance model.

Let us figure out together how to push this issue forward. Connect with us through one of many communication channels we established here.

Thank you for your patience :heart: