Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] incorrect names for newtyped map schemas #3835

Open
lgmyrek opened this issue Jun 11, 2024 · 2 comments
Open

[BUG] incorrect names for newtyped map schemas #3835

lgmyrek opened this issue Jun 11, 2024 · 2 comments
Assignees

Comments

@lgmyrek
Copy link

lgmyrek commented Jun 11, 2024

Tapir version: 1.10.8

Scala version: 2.13.14

Describe the bug
both Keys and Values are incorrectly named in map schemas - most probably also in other cases

resulting docs for code example:

openapi: 3.1.0
info:
  title: test
  version: v1
paths:
  /test/1:
    put:
      operationId: test1
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Map_Type_Type'
        required: true
      responses:
        '200':
          description: ''
        '400':
          description: 'Invalid value for: body'
          content:
            text/plain:
              schema:
                type: string
  /test/2:
    put:
      operationId: test2
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Map_Type_Type1'
        required: true
      responses:
        '200':
          description: ''
        '400':
          description: 'Invalid value for: body'
          content:
            text/plain:
              schema:
                type: string
  /test/3:
    put:
      operationId: test3
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Map_Type_Type2'
        required: true
      responses:
        '200':
          description: ''
        '400':
          description: 'Invalid value for: body'
          content:
            text/plain:
              schema:
                type: string
components:
  schemas:
    Map_Type_Type:
      title: Map_Type_Type
      type: object
      additionalProperties:
        type: integer
        format: int32
    Map_Type_Type1:
      title: Map_Type_Type
      type: object
      additionalProperties:
        type: integer
        format: int32
    Map_Type_Type2:
      title: Map_Type_Type
      type: object
      additionalProperties:
        type: integer
        format: int32

How to reproduce?

package com.test

import com.test.Defs._
import eu.timepit.refined.types.numeric.PosInt
import eu.timepit.refined.types.string.NonEmptyString
import io.circe.refined._
import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder}
import io.estatico.newtype.Coercible
import io.estatico.newtype.macros.newtype
import sttp.apispec.openapi.circe.yaml.RichOpenAPI
import sttp.tapir._
import sttp.tapir.codec.newtype.TapirCodecNewType
import sttp.tapir.codec.refined.TapirCodecRefined
import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter
import sttp.tapir.json.circe.jsonBody

object Test extends App  with TapirCodecRefined with TapirCodecNewType  with CirceCodecRefined with CirceNewtype {

  lazy implicit val map1Schema: Schema[Map[UserId, VeryImportantCount]] = Schema.schemaForMap[UserId, VeryImportantCount](_.value.value)
  lazy implicit val map2Schema: Schema[Map[AdminId, VeryImportantCount]] = Schema.schemaForMap[AdminId, VeryImportantCount](_.value.value)
  lazy implicit val map3Schema: Schema[Map[IntId, VeryImportantCount]] = Schema.schemaForMap[IntId, VeryImportantCount](_.value.value.toString)

  val endpoint1 = endpoint
    .put
    .name("test1")
    .in("test" / "1")
    .in(jsonBody[Map[UserId, VeryImportantCount]])

  val endpoint2 = endpoint
    .put
    .name("test2")
    .in("test" / "2")
    .in(jsonBody[Map[AdminId, VeryImportantCount]])

  val endpoint3 = endpoint
    .put
    .name("test3")
    .in("test" / "3")
    .in(jsonBody[Map[IntId, VeryImportantCount]])

  val endpoints = List(endpoint1, endpoint2, endpoint3)

  val yamlString: String = OpenAPIDocsInterpreter()
    .toOpenAPI(endpoints, "test", "v1")
    .toYaml

  println(yamlString)

}

object Defs {
  @newtype final case class UserId(value: NonEmptyString)
  @newtype final case class AdminId(value: NonEmptyString)
  @newtype final case class IntId(value: PosInt)

  @newtype final case class VeryImportantCount(value: Int)
}

trait CirceNewtype {
  implicit def coerceDecoder[R, N](
    implicit
    ev: Coercible[Decoder[R], Decoder[N]],
    R: Decoder[R],
  ): Decoder[N] = ev(R)

  implicit def coerceEncoder[R, N](
    implicit
    ev: Coercible[Encoder[R], Encoder[N]],
    R: Encoder[R],
  ): Encoder[N] = ev(R)

  implicit def coerceKeyDecoder[R, N](
    implicit
    ev: Coercible[KeyDecoder[R], KeyDecoder[N]],
    R: KeyDecoder[R],
  ): KeyDecoder[N] = ev(R)

  implicit def coerceKeyEncoder[R, N](
    implicit
    ev: Coercible[KeyEncoder[R], KeyEncoder[N]],
    R: KeyEncoder[R],
  ): KeyEncoder[N] = ev(R)

  implicit def coerceAsObjectEncoder[A, B](
    implicit
    ev: Coercible[Encoder.AsObject[A], Encoder.AsObject[B]],
    encA: Encoder.AsObject[A],
  ): Encoder.AsObject[B] = ev(Encoder.AsObject[A])
}

Additional information

@kciesielski kciesielski self-assigned this Jun 12, 2024
@kciesielski
Copy link
Member

kciesielski commented Jun 18, 2024

@lgmyrek thanks for reporting. This one is tricky to tackle. The @newtype macro generates code like

package object Defs {
  type UserId = UserId.Type
  object UserId {
    type Repr = String
    type Base = Any { type UserId$newtype }
    trait Tag extends Any
    type Type <: Base with Tag

    def apply(x: String): UserId = x.asInstanceOf[UserId]

    implicit final class Ops$newtype(val $this$: Type) extends AnyVal {
      def value: String = $this$.asInstanceOf[String]
    }
  }
}

I simplified the inner type to String but it doesn't change the fact, that the resolved type name will be UserId.Type, which in the end results in Map_Type_Type. I tried to find a way to work around this, but so far without success. Maybe it's the newtype library that could change the Type type to something like UserIdType 🤔

@lgmyrek
Copy link
Author

lgmyrek commented Jun 19, 2024

You could try to always name the schemas for new types eg:

  implicit def namedSchemaForNewType[A, B: WeakTypeTag](
    implicit
    ev: Coercible[Schema[A], Schema[B]],
    schema: Schema[A],
  ): Schema[B] = {
    val s = symbolOf[B]

    ev(schema).name(SName(s.fullName))
  }
  

tho this surely needs some work to also handle newtypes with generic params and I do not know the implications of always naming those for compatibility and other schema interactions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants