mcarleio / konvert

This kotlin compiler plugin is using KSP API and generates kotlin code to map one class to another
https://mcarleio.github.io/konvert/
Apache License 2.0
93 stars 8 forks source link

Allow ignoring properties that are not mapped directly #17

Closed msfjarvis closed 1 year ago

msfjarvis commented 1 year ago

I have a SavedPost class generated by my ORM client that represents a row in the database, and a couple of model classes that represent API responses and are nearly identical to these generated classes. Trying to map between them is proving to be a challenge due to the plugin seemingly requiring that every property in the source class has a mapping in the destination. Here's what I'm trying to do:

// "Source" class being generated by the ORM

public data class SavedPost(
  public val shortId: String,
  public val title: String,
  public val url: String,
  public val createdAt: String,
  public val commentCount: Int?,
  public val commentsUrl: String,
  public val submitterName: String,
  public val submitterAvatarUrl: String,
  public val tags: List<String>,
  public val description: String,
)

// "Target" class hand-written by me, used to model the API response
class LobstersPost(
  val shortId: String,
  val createdAt: String,
  val title: String,
  val url: String,
  val description: String,
  val commentCount: Int,
  val commentsUrl: String,
  @SerialName("submitter_user") val submitter: User,
  val tags: List<String>,
) {

  // How I'm trying to use Konvert
  @KonvertFrom(
    value = SavedPost::class,
    mappings =
      [
        Mapping(source = "submitterName", target = "submitter.username"),
        Mapping(source = "submitterAvatarUrl", target = "submitter.avatarUrl"),
      ],
  )
  companion object
}

As you can see, to simplify database storage I flattened out some nested fields which can be filled back in easily from the object received in the API response, but since the submitter object itself does not get mapped into a property in the SavedPost class I get an error like this from the processor:

e: [ksp] io.mcarle.konvert.processor.exceptions.PropertyMappingNotExistingException: No property for valueParameter=submitter existing in [PropertyMappingInfo(mappingParamName=savedPost, sourceName=submitterName, targetName=submitter.username, constant=null, expression=null, ignore=false, enableConverters=[], declaration=submitterName, isBasedOnAnnotation=true), PropertyMappingInfo(mappingParamName=savedPost, sourceName=submitterAvatarUrl, targetName=submitter.avatarUrl, constant=null, expression=null, ignore=false, enableConverters=[], declaration=submitterAvatarUrl, isBasedOnAnnotation=true), PropertyMappingInfo(mappingParamName=savedPost, sourceName=commentCount, targetName=commentCount, constant=null, expression=null, ignore=false, enableConverters=[], declaration=commentCount, isBasedOnAnnotation=false), PropertyMappingInfo(mappingParamName=savedPost, sourceName=commentsUrl, targetName=commentsUrl, constant=null, expression=null, ignore=false, enableConverters=[], declaration=commentsUrl, isBasedOnAnnotation=false), PropertyMappingInfo(mappingParamName=savedPost, sourceName=createdAt, targetName=createdAt, constant=null, expression=null, ignore=false, enableConverters=[], declaration=createdAt, isBasedOnAnnotation=false), PropertyMappingInfo(mappingParamName=savedPost, sourceName=description, targetName=description, constant=null, expression=null, ignore=false, enableConverters=[], declaration=description, isBasedOnAnnotation=false), PropertyMappingInfo(mappingParamName=savedPost, sourceName=shortId, targetName=shortId, constant=null, expression=null, ignore=false, enableConverters=[], declaration=shortId, isBasedOnAnnotation=false), PropertyMappingInfo(mappingParamName=savedPost, sourceName=submitterAvatarUrl, targetName=submitterAvatarUrl, constant=null, expression=null, ignore=false, enableConverters=[], declaration=submitterAvatarUrl, isBasedOnAnnotation=false), PropertyMappingInfo(mappingParamName=savedPost, sourceName=submitterName, targetName=submitterName, constant=null, expression=null, ignore=false, enableConverters=[], declaration=submitterName, isBasedOnAnnotation=false), PropertyMappingInfo(mappingParamName=savedPost, sourceName=tags, targetName=tags, constant=null, expression=null, ignore=false, enableConverters=[], declaration=tags, isBasedOnAnnotation=false), PropertyMappingInfo(mappingParamName=savedPost, sourceName=title, targetName=title, constant=null, expression=null, ignore=false, enableConverters=[], declaration=title, isBasedOnAnnotation=false), PropertyMappingInfo(mappingParamName=savedPost, sourceName=url, targetName=url, constant=null, expression=null, ignore=false, enableConverters=[], declaration=url, isBasedOnAnnotation=false)]
mcarleio commented 1 year ago

At the moment Konvert does not offer this "dot syntax" in source and target.

Instead you can use a custom expression for now:

@KonvertFrom(
  value = SavedPost::class,
  mappings = [
    Mapping(
      target = "submitter",
      expression = "package.of.User(username = it.submitterName, avatarUrl = it.submitterAvatarUrl)"
    )
  ]
)
companion object

This will generate the following:

fun LobstersPost.Companion.fromSavedPost(savedPost: SavedPost): LobstersPost = LobstersPost(
  // other mappings,
  submitter = savedPost.let { 
    package.of.User(username = it.submitterName, avatarUrl = it.submitterAvatarUrl)
  },
  // other mappings
)

You can also create a function which does the "complex" mapping stuff:

package package.of

fun userFromSavedPost(savedPost: SavedPost) = User(
   username = it.submitterName,
   avatarUrl = it.submitterAvatarUrl
)

and then call that function instead inside the expression:

@KonvertFrom(
  value = SavedPost::class,
  mappings = [
    Mapping(
      target = "submitter",
      expression = "package.of.userFromSavedPost(it)"
    )
  ]
)
companion object

This way, refactoring etc. will work more reliably. Only if you rename or move the function, you will have to change it in the expression as well, otherwise you will get an error after Konvert generated the mapping sources and during their compile-time.

Hope this helps for now?

msfjarvis commented 1 year ago

Okay reading back the OP it looks like I was trying to misuse Konvert and ended up confusing myself. What I wanted to do is to convert instances of LobstersPost and LobstersPostDetails to SavedPost, but KonvertFrom is actually meant for mapping in the opposite direction. I tried doing the same using KonvertTo but I get a different failure:

class LobstersPost(
  val shortId: String,
  val createdAt: String,
  val title: String,
  val url: String,
  val description: String,
  val commentCount: Int,
  val commentsUrl: String,
  @SerialName("submitter_user") val submitter: User,
  val tags: List<String>,
) {
  @KonvertTo(
    value = SavedPost::class,
    mappings =
      [
        Mapping(
          target = "submitter",
          expression = "dev.msfjarvis.claw.serialization.userFromSavedPost(it)"
        )
      ],
  )
  companion object
}
e: [ksp] java.lang.IllegalStateException: Mapping can only target classes and companion objects
mcarleio commented 1 year ago

The error message is a bit off, I will fix this... But the annotation @KonvertTo can only be applied to classes:

@KonvertTo(
    value = SavedPost::class,
    mappings =
      [
        Mapping(
          target = "submitter",
          expression = "dev.msfjarvis.claw.serialization.userFromSavedPost(it)"
        )
      ],
  )
class LobstersPost(
 // ...
)
msfjarvis commented 1 year ago

Thanks for the help! I was able to make everything work and delete my manual mapping functions https://msfjarvis.dev/g/compose-lobsters/25382acf0eaa