arainko / ducktape

Automatic and customizable compile time transformations between similar case classes and sealed traits/enums, essentially a thing that glues your code. Scala 3 only. Or is it duct 🤔
https://arainko.github.io/ducktape/
Other
402 stars 8 forks source link

Creating generic transformers #79

Closed brndt closed 11 months ago

brndt commented 11 months ago

Hello! I have one question regarding transformers. I have implemented Value Object pattern using opaque types so my validated data looks like this:

opaque type TeamName = String object TeamName: extension (value: TeamName) def value: String = value def create(value: String): IO[InvalidTeamNameError, TeamName] = ZIO.cond(value.inLengthRange(2 to 50), value, InvalidTeamNameError(value))

The problem that I have is that I can’t define generic transformers for these type of data because even if I extract ‘value’ method to common interface that is implemented by all necessary structure after, the generic transformers don’t work.

Screenshot 2023-09-20 at 19 30 17 Screenshot 2023-09-20 at 19 30 08

Is it somehow possible to create a generic transformer for this type of structures? I don't want to create a custom transformer for every opaque type.

PS: I've managed to resolve it by using case class and AnyVal as it is said in documentation but it would like to have a similar solution without overhead.

arainko commented 11 months ago

So generally it's not possible to abstract over 'raw' opaque types (or maybe it is, extensions do show up in the symbol list when reflected over :thinking:) but that is easily addressable if you sprinkle some newtype magic over what you're currently doing:

trait Newtype[A] {
  opaque type Type = A

  def apply(value: A): Type = value

  def wrapAll[F[_]](unwrapped: F[A]): F[Type] = unwrapped

  def unwrapAll[F[_]](wrapped: F[Type]): F[A] = wrapped

  extension (self: Type) {
    def value: A = self
  }

  given wrappingTransformer: Transformer[A, Type] = apply

  given uwnrappingTransformer: Transformer[Type, A] = _.value
}

Given the above your example would look like this (impl taken from here):

object TypeName extends Newtype[String]
export TypeName.Type as TypeName

and all of the Transformers declared inside Newtype are automatically available in implicit scope for TypeNames so now when you call team.to[TeamView] it should all be resolved for you.

You could also take a look at a Newtype that does validation here.

But yeah as I said abstracting over raw opaque types is still an open question (I haven't seen a library that does it, it'd have to be by some kind of arbitrary convention eg. when you find a single arg apply method on the companion you use it for wrapping and if a .value extension is found you use it for unwrapping, dunno if any of that is possible tho :smile:)

Hope I answered your question :heart:

brndt commented 11 months ago

arainko, thanks for such a detail response! Yes, the approach you've showed worked for me.

And also thanks for the library, it's very useful.

brndt commented 11 months ago

Seems like this approach doesn't work for enums:

Screenshot 2023-09-21 at 00 11 53
arainko commented 11 months ago

What's the expected semantic here? To generate a Transformer[TeamStatus, String] and vice versa I presume?

Oh actually after taking another look at it can you try this instead:

enum TeamStatus:
  case Enabled, Disabled

object TeamStatus extends Newtype[String](...)

Although I don't think newtypes are a good fit for this particular use case since what you're after in this case is an isomorphism between your enum and a String (or a refinement), a newtype isn't really that

brndt commented 11 months ago

I think this sadly will be a no go for enums, what's the expected semantic here? To generate a Transformer[TeamStatus, String] and vice versa I presume?

Yes. The only way I find it possible to do is by encapsulating enum in object, making something like this:

object TeamStatus
    extends Newtype[String](value => TeamStatus.Enum.values.has(value), value => InvalidTeamStatusError(value)):
  enum Enum:
    case Opened, Closed
export TeamStatus.Type as TeamStatus