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

Adding uniqueItems property to Set schemas #3604

Merged
merged 3 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions core/src/main/scala/sttp/tapir/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ object Schema extends LowPrioritySchema with SchemaCompanionMacros {
implicit def schemaForOption[T: Schema]: Schema[Option[T]] = implicitly[Schema[T]].asOption
implicit def schemaForArray[T: Schema]: Schema[Array[T]] = implicitly[Schema[T]].asArray
implicit def schemaForIterable[T: Schema, C[X] <: Iterable[X]]: Schema[C[T]] = implicitly[Schema[T]].asIterable[C]
implicit def schemaForSet[T: Schema, C[X] <: scala.collection.Set[X]]: Schema[C[T]] =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This relies on Scala implicit priority rules, specifically the fact that schemaForSet is "more specific" than schemaForIterable. It seems to work fine in tests, but I always feel a bit anxious when I need to rely on these rules, and in this case it seems to be the first implicit definition to do so.
I just hope we don't introduce some crazy implicit ambiguities into tapir user's projects...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'll find out soon enough :)

schemaForIterable[T, C].attribute(Schema.UniqueItems.Attribute, Schema.UniqueItems(true))
implicit def schemaForPart[T: Schema]: Schema[Part[T]] = implicitly[Schema[T]].map(_ => None)(_.body)

implicit def schemaForEither[A, B](implicit sa: Schema[A], sb: Schema[B]): Schema[Either[A, B]] = {
Expand Down Expand Up @@ -337,6 +339,11 @@ object Schema extends LowPrioritySchema with SchemaCompanionMacros {
object Title {
val Attribute: AttributeKey[Title] = new AttributeKey[Title]("sttp.tapir.Schema.Title")
}

case class UniqueItems(uniqueItems: Boolean)
object UniqueItems {
val Attribute: AttributeKey[UniqueItems] = new AttributeKey[UniqueItems]("sttp.tapir.Schema.UniqueItems")
}

/** @param typeParameterShortNames
* full name of type parameters, name is legacy and kept only for backward compatibility
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package sttp.tapir.docs.apispec.schema

import sttp.apispec.{Schema => ASchema, _}
import sttp.tapir.Schema.{SName, Title}
import sttp.tapir.Schema.{SName, Title, UniqueItems}
import sttp.tapir.Validator.EncodeToRaw
import sttp.tapir.docs.apispec.DocsExtensionAttribute.RichSchema
import sttp.tapir.docs.apispec.schema.TSchemaToASchema.{tDefaultToADefault, tExampleToAExample}
Expand Down Expand Up @@ -84,7 +84,7 @@ private[docs] class TSchemaToASchema(
var s = result
s = if (nullable) s.copy(nullable = Some(true)) else s
s = addMetadata(s, schema)
s = addTitle(s, schema)
s = addAttributes(s, schema)
s = addConstraints(s, primitiveValidators, schemaIsWholeNumber)
s
} else result
Expand All @@ -97,12 +97,14 @@ private[docs] class TSchemaToASchema(
.toListMap
}

private def addTitle(oschema: ASchema, tschema: TSchema[_]): ASchema = {
val fromAttr = tschema.attributes.get(Title.Attribute).map(_.value)
private def addAttributes(oschema: ASchema, tschema: TSchema[_]): ASchema = {
val titleFromAttr = tschema.attributes.get(Title.Attribute).map(_.value)
// The primary motivation for using schema name as fallback title is to improve Swagger UX with
// `oneOf` schemas in OpenAPI 3.1. See https://github.com/softwaremill/tapir/issues/3447 for details.
def fallback = tschema.name.map(fallbackSchemaTitle)
oschema.copy(title = fromAttr orElse fallback)
def fallbackTitle = tschema.name.map(fallbackSchemaTitle)
oschema
.copy(title = titleFromAttr orElse fallbackTitle)
.copy(uniqueItems = tschema.attribute(UniqueItems.Attribute).map(_.uniqueItems))
}

private def addMetadata(oschema: ASchema, tschema: TSchema[_]): ASchema = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
openapi: 3.1.0
info:
title: Entities
version: '1.0'
paths:
/:
get:
operationId: getRoot
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/ObjectWithSet'
components:
schemas:
FruitAmount:
title: FruitAmount
required:
- fruit
- amount
type: object
properties:
fruit:
type: string
amount:
type: integer
format: int32
ObjectWithSet:
title: ObjectWithSet
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/FruitAmount'
uniqueItems: true
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,15 @@ class VerifyYamlTest extends AnyFunSuite with Matchers {
actualYamlNoIndent shouldBe expectedYaml
}

test("should add uniqueItems for set-based array schema") {
val expectedYaml = load("expected_unfolded_array_with_unique_items.yml")

val actualYaml = OpenAPIDocsInterpreter().toOpenAPI(endpoint.out(jsonBody[ObjectWithSet]), Info("Entities", "1.0")).toYaml
val actualYamlNoIndent = noIndentation(actualYaml)

actualYamlNoIndent shouldBe expectedYaml
}

test("use fixed status code output in response") {
val expectedYaml = load("expected_fixed_status_code.yml")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ object VerifyYamlTestData {
case class G[T](data: T)
case class ObjectWrapper(value: FruitAmount)
case class ObjectWithList(data: List[FruitAmount])
case class ObjectWithSet(data: Set[FruitAmount])
case class ObjectWithOption(data: Option[FruitAmount])
case class ObjectWithDefaults(@default("foo") name: String, @default(12) count: Int)
}
2 changes: 1 addition & 1 deletion project/Versions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ object Versions {
val sttp = "3.9.4"
val sttpModel = "1.7.7"
val sttpShared = "1.3.17"
val sttpApispec = "0.7.4"
val sttpApispec = "0.8.0"
val akkaHttp = "10.2.10"
val akkaStreams = "2.6.20"
val pekkoHttp = "1.0.1"
Expand Down
Loading