Skip to content
Draft
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
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 @@ -51,7 +51,7 @@ private[tapir] object OneOfMacro {
val discriminatorName = _root_.sttp.tapir.FieldName($name, $conf.toEncodedName($name))
// cannot use .collect because of a bug in ScalaJS (Trying to access the this of another class ... during phase: jscode)
val discriminatorMapping = mappingAsMap.toList.flatMap {
case (k, _root_.sttp.tapir.Schema(_, _root_.scala.Some(fname), _, _, _, _, _, _, _, _, _)) => _root_.scala.collection.immutable.List($asString.apply(k) -> _root_.sttp.tapir.SchemaType.SRef(fname))
case (k, _root_.sttp.tapir.Schema(_, _root_.scala.Some(fname), _, _, _, _, _, _, _, _, _, _)) => _root_.scala.collection.immutable.List($asString.apply(k) -> _root_.sttp.tapir.SchemaType.SRef(fname))
case _ => _root_.scala.Nil
}
.toMap
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala-3/sttp/tapir/macros/SchemaMacros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ private[tapir] object SchemaCompanionMacros {
val mappingAsMap = mappingAsList.toMap

val discriminatorName = _root_.sttp.tapir.FieldName(${ Expr(functionName) }, $conf.toEncodedName(${ Expr(functionName) }))
val discriminatorMapping = mappingAsMap.collect { case (k, sf @ Schema(_, Some(fname), _, _, _, _, _, _, _, _, _)) =>
val discriminatorMapping = mappingAsMap.collect { case (k, sf @ Schema(_, Some(fname), _, _, _, _, _, _, _, _, _, _)) =>
$asString.apply(k) -> SRef(fname)
}

Expand Down
108 changes: 102 additions & 6 deletions core/src/main/scala/sttp/tapir/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,106 @@ case class Schema[T](
deprecated: Boolean = false,
hidden: Boolean = false,
validator: Validator[T] = Validator.pass[T],
attributes: AttributeMap = AttributeMap.Empty
attributes: AttributeMap = AttributeMap.Empty,
// The const value together with the value encoded to a raw format, which will then be directly rendered in the
// documentation. This is needed as codecs for nested types aren't available. Similar to Validator.EncodeToRaw
const: Option[(T, Option[Any])] = None
) extends SchemaMacros[T] {

def map[TT](f: T => Option[TT])(g: TT => T): Schema[TT] = copy(
// required for binary compatibility
def this(
schemaType: SchemaType[T],
name: Option[SName],
isOptional: Boolean,
description: Option[String],
default: Option[(T, Option[Any])],
format: Option[String],
encodedExample: Option[Any],
deprecated: Boolean,
hidden: Boolean,
validator: Validator[T],
attributes: AttributeMap
) =
this(schemaType, name, isOptional, description, default, format, encodedExample, deprecated, hidden, validator, attributes, None)

// def copy(
// schemaType: SchemaType[T] = this.schemaType,
// name: Option[SName] = this.name,
// isOptional: Boolean = this.isOptional,
// description: Option[String] = this.description,
// default: Option[(T, Option[Any])] = this.default,
// format: Option[String] = this.format,
// encodedExample: Option[Any] = this.encodedExample,
// deprecated: Boolean = this.deprecated,
// hidden: Boolean = this.hidden,
// validator: Validator[T] = this.validator,
// attributes: AttributeMap = this.attributes,
// const: Option[(T, Option[Any])] = this.const
// ): Schema[T] = new Schema(
// schemaType,
// name,
// isOptional,
// description,
// default,
// format,
// encodedExample,
// deprecated,
// hidden,
// validator,
// attributes,
// const
// )

// // required for binary compatibility
// def copy(
// schemaType: SchemaType[T],
// name: Option[SName],
// isOptional: Boolean,
// description: Option[String],
// default: Option[(T, Option[Any])],
// format: Option[String],
// encodedExample: Option[Any],
// deprecated: Boolean,
// hidden: Boolean,
// validator: Validator[T],
// attributes: AttributeMap
// ): Schema[T] =
// new Schema(
// schemaType,
// name,
// isOptional,
// description,
// default,
// format,
// encodedExample,
// deprecated,
// hidden,
// validator,
// attributes,
// this.const
// )

def map[TT](f: T => Option[TT])(g: TT => T): Schema[TT] = Schema(
schemaType = schemaType.contramap(g),
default = default.flatMap { case (t, raw) =>
f(t).map(tt => (tt, raw))
},
validator = validator.contramap(g)
validator = validator.contramap(g),
const = const.flatMap { case (t, raw) =>
f(t).map(tt => (tt, raw))
}
)

/** Adapt this schema to type `TT`. Only the meta-data is retained, except for default values and the validator (however, product
* field/subtypes validators are retained). Run-time functionality:
* - traversing collection elements, product fields, or coproduct subtypes
* - validating an instance of type `TT` the top-level type is lost.
*/
def as[TT]: Schema[TT] = copy(
def as[TT]: Schema[TT] = Schema(
schemaType = schemaType.as[TT],
default = None,
validator = Validator.pass
validator = Validator.pass,
const = None
)

/** Returns an optional version of this schema, with `isOptional` set to true. */
Expand Down Expand Up @@ -257,6 +337,22 @@ case class Schema[T](
}

object Schema extends LowPrioritySchema with SchemaCompanionMacros {
// // required for binary compatibility
// def apply[T](
// schemaType: SchemaType[T],
// name: Option[SName],
// isOptional: Boolean,
// description: Option[String],
// default: Option[(T, Option[Any])],
// format: Option[String],
// encodedExample: Option[Any],
// deprecated: Boolean,
// hidden: Boolean,
// validator: Validator[T],
// attributes: AttributeMap
// ): Schema[T] =
// new Schema(schemaType, name, isOptional, description, default, format, encodedExample, deprecated, hidden, validator, attributes, None)

val ModifyCollectionElements = "each"

/** Creates a schema for type `T`, where the low-level representation is a `String`. */
Expand Down Expand Up @@ -309,7 +405,7 @@ object Schema extends LowPrioritySchema with SchemaCompanionMacros {

implicit def schemaForEither[A, B](implicit sa: Schema[A], sb: Schema[B]): Schema[Either[A, B]] = {
Schema[Either[A, B]](
SchemaType.SCoproduct(List(sa, sb), None) {
SchemaType.SCoproduct[Either[A, B]](List(sa, sb), None) {
case Left(v) => Some(SchemaWithValue(sa, v))
case Right(v) => Some(SchemaWithValue(sb, v))
},
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala/sttp/tapir/SchemaType.scala
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ object SchemaType {
): SCoproduct[T] = {
SCoproduct(
subtypes.map {
case s @ Schema(st: SchemaType.SProduct[Any @unchecked], _, _, _, _, _, _, _, _, _, _)
case s @ Schema(st: SchemaType.SProduct[Any @unchecked], _, _, _, _, _, _, _, _, _, _, _)
if st.fields.forall(_.name != discriminatorName) =>
s.copy(schemaType = st.copy(fields = st.fields :+ SProductField[Any, D](discriminatorName, discriminatorSchema, _ => None)))
case s => s
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import sttp.apispec.{Schema => ASchema, _}
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}
import sttp.tapir.docs.apispec.schema.TSchemaToASchema.{tConstToAConst, tDefaultToADefault, tExampleToAExample}
import sttp.tapir.docs.apispec.{DocsExtensions, exampleValue}
import sttp.tapir.internal._
import sttp.tapir.{Codec, Validator, Schema => TSchema, SchemaType => TSchemaType}
Expand Down Expand Up @@ -43,7 +43,7 @@ private[docs] class TSchemaToASchema(
properties = extractProperties(fields)
)
case TSchemaType.SArray(el) => ASchema(SchemaType.Array).copy(items = Some(apply(el, allowReference = true)))
case opt @ TSchemaType.SOption(nested @ TSchema(_, Some(name), _, _, _, _, _, _, _, _, _)) =>
case opt @ TSchemaType.SOption(nested @ TSchema(_, Some(name), _, _, _, _, _, _, _, _, _, _)) =>
// #3288: in case there are multiple different customisations of the nested schema, we need to propagate the
// metadata to properly customise the reference. These are also propagated in ToKeyedSchemas when computing
// the initial list of schemas.
Expand Down Expand Up @@ -121,6 +121,7 @@ private[docs] class TSchemaToASchema(
oschema.copy(
description = tschema.description.orElse(oschema.description),
default = tDefaultToADefault(tschema).orElse(oschema.default),
const = tConstToAConst(tschema).orElse(oschema.const),
examples = tExampleToAExample(tschema).map(List(_)).orElse(oschema.examples),
format = tschema.format.orElse(oschema.format),
deprecated = (if (tschema.deprecated) Some(true) else None).orElse(oschema.deprecated),
Expand Down Expand Up @@ -193,5 +194,10 @@ object TSchemaToASchema {
def tDefaultToADefault(schema: TSchema[_]): Option[ExampleValue] = schema.default.flatMap { case (_, raw) =>
raw.flatMap(r => exampleValue(schema, r))
}

def tConstToAConst(schema: TSchema[_]): Option[ExampleValue] = schema.const.flatMap { case (_, raw) =>
raw.flatMap(r => exampleValue(schema, r))
}

def tExampleToAExample(schema: TSchema[_]): Option[ExampleValue] = schema.encodedExample.flatMap(exampleValue(schema, _))
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ private[docs] object ToKeyedSchemas {
def apply(schema: TSchema[_]): List[KeyedSchema] = {
val thisSchema = SchemaKey(schema).map(_ -> schema).toList
val nestedSchemas = schema match {
case TSchema(TSchemaType.SArray(o), _, _, _, _, _, _, _, _, _, _) => apply(o)
case t @ TSchema(o: TSchemaType.SOption[_, _], _, _, _, _, _, _, _, _, _, _) =>
case TSchema(TSchemaType.SArray(o), _, _, _, _, _, _, _, _, _, _, _) => apply(o)
case t @ TSchema(o: TSchemaType.SOption[_, _], _, _, _, _, _, _, _, _, _, _, _) =>
// #1168: if there's an optional field which is an object, with metadata defined (such as description), this
// needs to be propagated to the target object, so that it isn't omitted.
apply(propagateMetadataForOption(t, o).element)
case TSchema(st: TSchemaType.SProduct[_], _, _, _, _, _, _, _, _, _, _) => productSchemas(st)
case TSchema(st: TSchemaType.SCoproduct[_], _, _, _, _, _, _, _, _, _, _) => coproductSchemas(st)
case TSchema(st: TSchemaType.SOpenProduct[_, _], _, _, _, _, _, _, _, _, _, _) => apply(st.valueSchema)
case TSchema(st: TSchemaType.SProduct[_], _, _, _, _, _, _, _, _, _, _, _) => productSchemas(st)
case TSchema(st: TSchemaType.SCoproduct[_], _, _, _, _, _, _, _, _, _, _, _) => coproductSchemas(st)
case TSchema(st: TSchemaType.SOpenProduct[_, _], _, _, _, _, _, _, _, _, _, _, _) => apply(st.valueSchema)
case _ => List.empty
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
openapi: 3.1.0
info:
title: Entities
version: '1.0'
paths:
/:
get:
operationId: getRoot
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/FruitConst'
components:
schemas:
FruitConst:
title: FruitConst
type: object
required:
- fruitType
properties:
fruitType:
type: string
const: Golden Delicious
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ package sttp.tapir.docs.openapi
import io.circe.generic.auto._
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import sttp.capabilities.Streams
import sttp.apispec.openapi.Info
import sttp.apispec.openapi.circe.yaml._
import sttp.capabilities.Streams
import sttp.tapir.EndpointIO.Example
import sttp.tapir.docs.openapi.dtos.{Author, Book, Country, Genre}
import sttp.tapir.generic.Derived
import sttp.tapir.generic.auto._
import sttp.tapir.json.circe._
import sttp.tapir.tests.data.{Entity, Organization, Person}
import sttp.tapir.tests.data.{Entity, FruitConst, Organization, Person}
import sttp.tapir.{endpoint, _}

import java.nio.ByteBuffer
Expand All @@ -20,6 +20,19 @@ import java.time.ZonedDateTime

class VerifyYamlExampleTest extends AnyFunSuite with Matchers {

test("support const attribute") {
val expectedYaml = load("example/expected_single_example_with_const.yml")
val actualYaml = OpenAPIDocsInterpreter()
.toOpenAPI(
endpoint.get.out(jsonBody[FruitConst]),
Info("Entities", "1.0")
)
.toYaml

val actualYamlNoIndent = noIndentation(actualYaml)
actualYamlNoIndent shouldBe expectedYaml
}

test("support example of list and not-list types") {
val expectedYaml = load("example/expected_examples_of_list_and_not_list_types.yml")
val actualYaml = OpenAPIDocsInterpreter()
Expand Down
11 changes: 11 additions & 0 deletions tests/src/main/scala/sttp/tapir/tests/data/FruitConst.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package sttp.tapir.tests.data

import sttp.tapir.Schema

case class FruitConst(fruitType: String)

object FruitConst {
implicit val schemw: Schema[FruitConst] = Schema
.derived[FruitConst]
.modifyUnsafe[String]("fruitType")(s => s.copy(const = Some(("Golden Delicious", Some("Golden Delicious")))))
}