Skip to content

Commit

Permalink
Properly derive schemas for tuples (#3954)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw authored Jul 25, 2024
1 parent 5c3c60d commit 33dfeba
Show file tree
Hide file tree
Showing 5 changed files with 18 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ trait SchemaMagnoliaDerivation {

def join[T](ctx: ReadOnlyCaseClass[Schema, T])(implicit genericDerivationConfig: Configuration): Schema[T] = {
withCache(ctx.typeName, ctx.annotations) {
val result =
var result =
if (ctx.isValueClass) {
require(ctx.parameters.nonEmpty, s"Cannot derive schema for generic value class: ${ctx.typeName.owner}")
val valueSchema = ctx.parameters.head.typeclass
Expand All @@ -24,6 +24,9 @@ trait SchemaMagnoliaDerivation {
// Not using inherited annotations when generating type name, we don't want @encodedName to be inherited for types
Schema[T](schemaType = productSchemaType(ctx), name = Some(typeNameToSchemaName(ctx.typeName, ctx.annotations)))
}
if (ctx.typeName.full.startsWith("scala.Tuple")) {
result = result.attribute(Schema.Tuple.Attribute, Schema.Tuple(true))
}
enrichSchema(result, mergeAnnotations(ctx.annotations, ctx.inheritedAnnotations))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ trait SchemaMagnoliaDerivation {

override def join[T](ctx: CaseClass[Schema, T]): Schema[T] = {
withCache(ctx.typeInfo, ctx.annotations) {
val result =
var result =
if (ctx.isValueClass) {
require(ctx.params.nonEmpty, s"Cannot derive schema for generic value class: ${ctx.typeInfo.owner}")
val valueSchema = ctx.params.head.typeclass
Expand All @@ -27,6 +27,9 @@ trait SchemaMagnoliaDerivation {
// Not using inherited annotations when generating type name, we don't want @encodedName to be inherited for types
Schema[T](schemaType = productSchemaType(ctx), name = Some(typeNameToSchemaName(ctx.typeInfo, ctx.annotations)))
}
if (ctx.typeInfo.full.startsWith("scala.Tuple")) {
result = result.attribute(Schema.Tuple.Attribute, Schema.Tuple(true))
}
enrichSchema(result, mergeAnnotations(ctx.annotations, ctx.inheritedAnnotations))
}
}
Expand Down
14 changes: 7 additions & 7 deletions core/src/main/scala/sttp/tapir/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -350,15 +350,15 @@ object Schema extends LowPrioritySchema with SchemaCompanionMacros {
val Attribute: AttributeKey[UniqueItems] = new AttributeKey[UniqueItems]("sttp.tapir.Schema.UniqueItems")
}

/** Hints that a [[SchemaType.SProduct]] should be rendered in the schema as an `array` (#3941).
/** Specifies that the given schema is for a tuple. Tuples are products with no meaningful property names - attributes are identified by
* their position.
*
* Used to model tuples, which are products with no meaningful property names - attributes are identified by their position. When
* converting to JSON schema, adding this attribute on a schema for a `SProduct` renders `array` schema, with type constraints for each
* index.
* When converting a tuple schema of type [[SchemaType.SProduct]] to JSON schema, renders as an `array` schema, with type constraints for
* each index (#3941).
*/
case class ProductAsArray(productAsArray: Boolean)
object ProductAsArray {
val Attribute: AttributeKey[ProductAsArray] = new AttributeKey[ProductAsArray]("sttp.tapir.Schema.ProductAsArray")
case class Tuple(isTuple: Boolean)
object Tuple {
val Attribute: AttributeKey[Tuple] = new AttributeKey[Tuple]("sttp.tapir.Schema.Tuple")
}

/** @param typeParameterShortNames
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ private[docs] class TSchemaToASchema(
case TSchemaType.SNumber() => ASchema(SchemaType.Number)
case TSchemaType.SBoolean() => ASchema(SchemaType.Boolean)
case TSchemaType.SString() => ASchema(SchemaType.String)
case TSchemaType.SProduct(fields) if schema.attribute(TSchema.ProductAsArray.Attribute).map(_.productAsArray).getOrElse(false) =>
case TSchemaType.SProduct(fields) if schema.attribute(TSchema.Tuple.Attribute).map(_.isTuple).getOrElse(false) =>
ASchema(SchemaType.Array).copy(
prefixItems = Some(fields.map(f => apply(f.schema, allowReference = true)))
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import sttp.tapir.Schema.annotations.title
import sttp.tapir._
import sttp.tapir.generic.auto._

class JsonSchemasTest extends AnyFlatSpec with Matchers with OptionValues with EitherValues with Inside {
class TapirSchemaToJsonSchemaTest extends AnyFlatSpec with Matchers with OptionValues with EitherValues with Inside {
behavior of "TapirSchemaToJsonSchema"

it should "represent schema as JSON" in {
Expand Down Expand Up @@ -150,9 +150,8 @@ class JsonSchemasTest extends AnyFlatSpec with Matchers with OptionValues with E
TapirSchemaToJsonSchema(schema2, true).title shouldBe Some("Map_Either_Int_Node_Int_String")
}

it should "Generate array for products marked with ProductAsArray attribute" in {
it should "Generate array for products marked with Tuple attribute" in {
val tSchema: Schema[(Int, String)] = implicitly[Schema[(Int, String)]]
.attribute(Schema.ProductAsArray.Attribute, Schema.ProductAsArray(true))

// when
val result = TapirSchemaToJsonSchema(tSchema, markOptionsAsNullable = false)
Expand Down

0 comments on commit 33dfeba

Please sign in to comment.