Skip to content

Commit

Permalink
Adding uniqueItems property to Set schemas (#3604)
Browse files Browse the repository at this point in the history
  • Loading branch information
ghik committed Mar 14, 2024
1 parent e611d21 commit 08628a8
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 7 deletions.
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]] =
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

0 comments on commit 08628a8

Please sign in to comment.