Skip to content

Commit

Permalink
Allow to retrieve schema type parameters full name (#3500)
Browse files Browse the repository at this point in the history
  • Loading branch information
yakivy authored Feb 9, 2024
1 parent 8e946f4 commit fee45e2
Show file tree
Hide file tree
Showing 16 changed files with 91 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ trait SchemaMagnoliaDerivation {
case Some(altName) =>
Schema.SName(altName, Nil)
case None =>
Schema.SName(typeName.full, allTypeArguments(typeName).map(_.short).toList)
Schema.SName(typeName.full, allTypeArguments(typeName).map(_.full).toList)
}
}

Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala-2/sttp/tapir/internal/OneOfMacro.scala
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,6 @@ private[tapir] object OneOfMacro {

private def extractTypeArguments(c: blackbox.Context)(weakType: c.Type): List[String] = {
def allTypeArguments(tn: c.Type): Seq[c.Type] = tn.typeArgs.flatMap(tn2 => tn2 +: allTypeArguments(tn2))
allTypeArguments(weakType).map(_.typeSymbol.name.decodedName.toString).toList
allTypeArguments(weakType).map(_.typeSymbol.fullName).toList
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,17 @@ private[tapir] object SchemaMapMacro {

def extractTypeArguments(weakType: c.Type): List[String] = {
def allTypeArguments(tn: c.Type): Seq[c.Type] = tn.typeArgs.flatMap(tn2 => tn2 +: allTypeArguments(tn2))
allTypeArguments(weakType).map(_.typeSymbol.name.decodedName.toString).toList
allTypeArguments(weakType).map(_.typeSymbol.fullName).toList
}

val weakTypeV = weakTypeOf[V]
val weakTypeK = weakTypeOf[K]

val keyTypeParameter = weakTypeK.typeSymbol.name.decodedName.toString
val keyTypeParameter = weakTypeK.typeSymbol.fullName

val genericTypeParameters = (if (keyTypeParameter == "String") Nil else List(keyTypeParameter)) ++ extractTypeArguments(weakTypeK) ++
List(weakTypeV.typeSymbol.name.decodedName.toString) ++ extractTypeArguments(weakTypeV)
val genericTypeParameters =
(if (keyTypeParameter.split('.').lastOption.contains("String")) Nil else List(keyTypeParameter)) ++
extractTypeArguments(weakTypeK) ++ List(weakTypeV.typeSymbol.fullName) ++ extractTypeArguments(weakTypeV)
val schemaForMap =
q"""{
val s = $schemaForV
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ trait SchemaMagnoliaDerivation {
case Some(altName) =>
Schema.SName(altName, Nil)
case None =>
Schema.SName(typeName.full, allTypeArguments(typeName).map(_.short).toList)
Schema.SName(typeName.full, allTypeArguments(typeName).map(_.full).toList)
}
}

Expand Down
9 changes: 6 additions & 3 deletions core/src/main/scala-3/sttp/tapir/internal/SNameMacros.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package sttp.tapir.internal

import scala.quoted.*
import sttp.tapir.SchemaType.*

// TODO: make private[tapir] once Scala3 compilation is fixed
object SNameMacros {
Expand All @@ -21,9 +20,13 @@ object SNameMacros {
else if sym == defn.EmptyPackageClass then List.empty
else if sym == defn.RootPackage then List.empty
else if sym == defn.RootClass then List.empty
else if sym.name.matches("<[^>]+>") then nameChain(sym.owner)
else nameChain(sym.owner) :+ normalizedName(sym)

nameChain(tpe.typeSymbol).mkString(".")
nameChain(tpe.typeSymbol).mkString(".") match {
case "scala.Predef.String" => "java.lang.String"
case other => other
}
}

def extractTypeArguments(using q: Quotes)(tpe: q.reflect.TypeRepr): List[String] = {
Expand All @@ -34,7 +37,7 @@ object SNameMacros {
case _ => List.empty[TypeRepr]
}

allTypeArguments(tpe).map(_.typeSymbol.name).toList
allTypeArguments(tpe).map(typeFullNameFromTpe).toList
}

}
7 changes: 4 additions & 3 deletions core/src/main/scala-3/sttp/tapir/macros/SchemaMacros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,12 @@ private[tapir] object SchemaCompanionMacros {
import quotes.reflect.*

val ktpe = TypeRepr.of[K]
val ktpeName = ktpe.typeSymbol.name
val ktpeName = SNameMacros.typeFullNameFromTpe(ktpe)
val vtpe = TypeRepr.of[V]

val genericTypeParameters = (if (ktpeName == "String") Nil else List(ktpeName)) ++ SNameMacros.extractTypeArguments(ktpe) ++
List(vtpe.typeSymbol.name) ++ SNameMacros.extractTypeArguments(vtpe)
val genericTypeParameters = (if (ktpeName.split('.').lastOption.contains("String")) Nil else List(ktpeName)) ++
SNameMacros.extractTypeArguments(ktpe) ++ List(SNameMacros.typeFullNameFromTpe(vtpe)) ++
SNameMacros.extractTypeArguments(vtpe)

'{
Schema(
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/scala/sttp/tapir/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,9 @@ object Schema extends LowPrioritySchema with SchemaCompanionMacros {
val Attribute: AttributeKey[Title] = new AttributeKey[Title]("sttp.tapir.Schema.Title")
}

/** @param typeParameterShortNames
* full name of type parameters, name is legacy and kept only for backward compatibility
*/
case class SName(fullName: String, typeParameterShortNames: List[String] = Nil) {
def show: String = fullName + (if (typeParameterShortNames.isEmpty) "" else typeParameterShortNames.mkString("[", ",", "]"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class LegacySchemaGenericAutoTest extends AsyncFlatSpec with Matchers {

it should "find schema for map of value classes" in {
val schema = implicitly[Schema[Map[String, IntegerValueClass]]]
schema.name shouldBe Some(SName("Map", List("IntegerValueClass")))
schema.name shouldBe Some(SName("Map", List("sttp.tapir.generic.IntegerValueClass")))
schema.schemaType shouldBe SOpenProduct[Map[String, IntegerValueClass], IntegerValueClass](
Nil,
Schema(SInteger(), format = Some("int32"))
Expand Down
4 changes: 2 additions & 2 deletions core/src/test/scala-3/sttp/tapir/SchemaMacroScala3Test.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class SchemaMacroScala3Test extends AnyFlatSpec with Matchers:
val s: Schema[List[String] | List[Int]] = Schema.derivedUnion[List[String] | List[Int]]

// then
s.name.map(_.show) shouldBe Some("scala.collection.immutable.List[String]_or_scala.collection.immutable.List[Int]")
s.name.map(_.show) shouldBe Some("scala.collection.immutable.List[java.lang.String]_or_scala.collection.immutable.List[scala.Int]")

s.schemaType should matchPattern { case SchemaType.SCoproduct(_, _) => }
val coproduct = s.schemaType.asInstanceOf[SchemaType.SCoproduct[List[String] | List[Int]]]
Expand All @@ -64,7 +64,7 @@ class SchemaMacroScala3Test extends AnyFlatSpec with Matchers:
val s: Schema[List[String] | Vector[Int]] = Schema.derivedUnion[List[String] | Vector[Int]]

// then
s.name.map(_.show) shouldBe Some("scala.collection.immutable.List[String]_or_scala.collection.immutable.Vector[Int]")
s.name.map(_.show) shouldBe Some("scala.collection.immutable.List[java.lang.String]_or_scala.collection.immutable.Vector[scala.Int]")

s.schemaType should matchPattern { case SchemaType.SCoproduct(_, _) => }
val coproduct = s.schemaType.asInstanceOf[SchemaType.SCoproduct[List[String] | Vector[Int]]]
Expand Down
12 changes: 8 additions & 4 deletions core/src/test/scala/sttp/tapir/SchemaMacroTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ class SchemaMacroTest extends AnyFlatSpec with Matchers with TableDrivenProperty
FieldName("v"),
Schema(
SOpenProduct[Map[String, Person], Person](Nil, implicitly[Schema[Person]].description("test"))(identity),
Some(SName("Map", List("Person")))
Some(SName("Map", List("sttp.tapir.SchemaMacroTestData.Person")))
)
)
)
Expand All @@ -183,7 +183,7 @@ class SchemaMacroTest extends AnyFlatSpec with Matchers with TableDrivenProperty
// then
schema shouldBe Schema(
SOpenProduct(Nil, implicitly[Schema[V]])((_: Map[String, V]) => Map.empty),
name = Some(SName("Map", List("V")))
name = Some(SName("Map", List("sttp.tapir.SchemaMacroTest.V")))
)

schema.schemaType.asInstanceOf[SOpenProduct[Map[String, V], V]].mapFieldValues(Map("k" -> V())) shouldBe Map("k" -> V())
Expand All @@ -200,7 +200,9 @@ class SchemaMacroTest extends AnyFlatSpec with Matchers with TableDrivenProperty
// then
schema shouldBe Schema(
SOpenProduct(Nil, implicitly[Schema[V]])((_: Map[K, V]) => Map.empty),
name = Some(SName("Map", List("K", "V")))
name = Some(
SName("Map", List("sttp.tapir.SchemaMacroTest.K", "sttp.tapir.SchemaMacroTest.V"))
)
)

schema.schemaType.asInstanceOf[SOpenProduct[Map[K, V], V]].mapFieldValues(Map(K() -> V())) shouldBe Map("k" -> V())
Expand Down Expand Up @@ -278,7 +280,9 @@ class SchemaMacroTest extends AnyFlatSpec with Matchers with TableDrivenProperty
)
)

schema.name shouldBe Some(SName("sttp.tapir.SchemaMacroTestData.WrapperT", List("String", "Int", "String")))
schema.name shouldBe Some(
SName("sttp.tapir.SchemaMacroTestData.WrapperT", List("java.lang.String", "scala.Int", "java.lang.String"))
)
}

it should "add the discriminator as a field when using oneOfUsingField" in {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,19 +93,19 @@ class SchemaGenericAutoTest extends AsyncFlatSpec with Matchers {

it should "derive schema for parametrised type classes" in {
val schema = implicitly[Schema[H[A]]]
schema.name shouldBe Some(SName("sttp.tapir.generic.H", List("A")))
schema.name shouldBe Some(SName("sttp.tapir.generic.H", List("sttp.tapir.generic.A")))
schema.schemaType shouldBe SProduct[H[A]](List(field(FieldName("data"), expectedASchema)))
}

it should "find schema for map" in {
val schema = implicitly[Schema[Map[String, Int]]]
schema.name shouldBe Some(SName("Map", List("Int")))
schema.name shouldBe Some(SName("Map", List("scala.Int")))
schema.schemaType shouldBe SOpenProduct[Map[String, Int], Int](Nil, intSchema)(identity)
}

it should "find schema for map of products" in {
val schema = implicitly[Schema[Map[String, D]]]
schema.name shouldBe Some(SName("Map", List("D")))
schema.name shouldBe Some(SName("Map", List("sttp.tapir.generic.D")))
schema.schemaType shouldBe SOpenProduct[Map[String, D], D](
Nil,
Schema(SProduct(List(field(FieldName("someFieldName"), stringSchema))), Some(SName("sttp.tapir.generic.D")))
Expand All @@ -114,7 +114,7 @@ class SchemaGenericAutoTest extends AsyncFlatSpec with Matchers {

it should "find schema for map of generic products" in {
val schema = implicitly[Schema[Map[String, H[D]]]]
schema.name shouldBe Some(SName("Map", List("H", "D")))
schema.name shouldBe Some(SName("Map", List("sttp.tapir.generic.H", "sttp.tapir.generic.D")))
schema.schemaType shouldBe SOpenProduct[Map[String, H[D]], H[D]](
Nil,
Schema(
Expand All @@ -126,7 +126,7 @@ class SchemaGenericAutoTest extends AsyncFlatSpec with Matchers {
)
)
),
Some(SName("sttp.tapir.generic.H", List("D")))
Some(SName("sttp.tapir.generic.H", List("sttp.tapir.generic.D")))
)
)(identity)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import sttp.tapir.{SchemaType => TSchemaType, Schema => TSchema}
package object schema {

private[docs] val defaultSchemaName: TSchema.SName => String = info => {
val shortName = info.fullName.split('.').last
(shortName +: info.typeParameterShortNames).mkString("_")
def prepareName(name: String) = name.split('.').last
(info.fullName +: info.typeParameterShortNames).map(prepareName).mkString("_")
}

/*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
openapi: 3.1.0
info:
title: Fruits
version: '1.0'
paths:
/:
get:
operationId: getRoot
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/Map_sttp_tapir_tests_data_FruitAmount'
components:
schemas:
Map_sttp_tapir_tests_data_FruitAmount:
type: object
additionalProperties:
$ref: '#/components/schemas/sttp_tapir_tests_data_FruitAmount'
sttp_tapir_tests_data_FruitAmount:
required:
- fruit
- amount
type: object
properties:
fruit:
type: string
amount:
type: integer
format: int32
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,19 @@ class VerifyYamlTest extends AnyFunSuite with Matchers {
actualYamlNoIndent shouldBe expectedYaml
}

test("should generate full schema name for type params") {
val e = endpoint.out(jsonBody[Map[String, FruitAmount]])
val expectedYaml = load("expected_full_schema_names.yml")

val options = OpenAPIDocsOptions.default.copy(schemaName = info => {
(info.fullName +: info.typeParameterShortNames).flatMap(_.split('.')).mkString("_")
})

val actualYaml = OpenAPIDocsInterpreter(options).toOpenAPI(e, Info("Fruits", "1.0")).toYaml
val actualYamlNoIndent = noIndentation(actualYaml)
actualYamlNoIndent shouldBe expectedYaml
}

test("should generate default and example values for nested optional fields") {
case class Nested(nestedValue: String)
case class ClassWithNestedOptionalField(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ private class SchemaDerivation(genericDerivationConfig: Expr[Configuration])(usi
encodedName match
case None =>
def allTypeArguments(tn: TypeInfo): Seq[TypeInfo] = tn.typeParams.toList.flatMap(tn2 => tn2 +: allTypeArguments(tn2))
'{ Schema.SName(${ Expr(typeInfo.full) }, ${ Expr.ofList(allTypeArguments(typeInfo).map(_.short).toList.map(Expr(_))) }) }
'{ Schema.SName(${ Expr(typeInfo.full) }, ${ Expr.ofList(allTypeArguments(typeInfo).map(_.full).toList.map(Expr(_))) }) }
case Some(en) =>
'{ Schema.SName($en, Nil) }

Expand Down Expand Up @@ -154,7 +154,7 @@ private class SchemaDerivation(genericDerivationConfig: Expr[Configuration])(usi
def topLevelEncodedName: Option[Expr[String]] = findEncodedName(topLevel)

def encodedName: Option[Expr[String]] = findEncodedName(all)

private def findEncodedName(terms: List[Term]): Option[Expr[String]] = terms
.map(_.asExpr)
.collectFirst { case '{ $en: Schema.annotations.encodedName } => en }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,19 +100,19 @@ class SchemaGenericAutoTest extends AsyncFlatSpec with Matchers with Inside {

it should "derive schema for parametrised type classes" in {
val schema = implicitlySchema[H[A]]
schema.name shouldBe Some(SName("sttp.tapir.json.pickler.H", List("A")))
schema.name shouldBe Some(SName("sttp.tapir.json.pickler.H", List("sttp.tapir.json.pickler.A")))
schema.schemaType shouldBe SProduct[H[A]](List(field(FieldName("data"), expectedASchema)))
}

it should "find schema for map" in {
val schema = implicitlySchema[Map[String, Int]]
schema.name shouldBe Some(SName("Map", List("Int")))
schema.name shouldBe Some(SName("Map", List("scala.Int")))
schema.schemaType shouldBe SOpenProduct[Map[String, Int], Int](Nil, intSchema)(identity)
}

it should "find schema for map of products" in {
val schema = implicitlySchema[Map[String, D]]
schema.name shouldBe Some(SName("Map", List("D")))
schema.name shouldBe Some(SName("Map", List("sttp.tapir.json.pickler.D")))
schema.schemaType shouldBe SOpenProduct[Map[String, D], D](
Nil,
Schema(SProduct(List(field(FieldName("someFieldName"), stringSchema))), Some(SName("sttp.tapir.json.pickler.D")))
Expand All @@ -121,7 +121,7 @@ class SchemaGenericAutoTest extends AsyncFlatSpec with Matchers with Inside {

it should "find schema for map of generic products" in {
val schema = implicitlySchema[Map[String, H[D]]]
schema.name shouldBe Some(SName("Map", List("H", "D")))
schema.name shouldBe Some(SName("Map", List("sttp.tapir.json.pickler.H", "sttp.tapir.json.pickler.D")))
schema.schemaType shouldBe SOpenProduct[Map[String, H[D]], H[D]](
Nil,
Schema(
Expand All @@ -133,7 +133,7 @@ class SchemaGenericAutoTest extends AsyncFlatSpec with Matchers with Inside {
)
)
),
Some(SName("sttp.tapir.json.pickler.H", List("D")))
Some(SName("sttp.tapir.json.pickler.H", List("sttp.tapir.json.pickler.D")))
)
)(identity)
}
Expand Down

0 comments on commit fee45e2

Please sign in to comment.