Skip to content

Commit

Permalink
Merge branch 'master' into feat/add-zio-multipart-body-support
Browse files Browse the repository at this point in the history
  • Loading branch information
seakayone authored Jul 4, 2024
2 parents fa9c049 + 91a95b7 commit 50919d2
Show file tree
Hide file tree
Showing 91 changed files with 2,668 additions and 457 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ tapir documentation is available at [tapir.softwaremill.com](http://tapir.softwa
Add the following dependency:

```sbt
"com.softwaremill.sttp.tapir" %% "tapir-core" % "1.10.10"
"com.softwaremill.sttp.tapir" %% "tapir-core" % "1.10.12"
```

Then, import:
Expand Down
49 changes: 44 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,17 @@ lazy val core: ProjectMatrix = (projectMatrix in file("core"))
)
)
)
.nativePlatform(
scalaVersions = List(scala3),
settings = {
commonNativeSettings ++ Seq(
libraryDependencies ++= Seq(
"io.github.cquiroz" %%% "scala-java-time" % Versions.nativeScalaJavaTime,
"io.github.cquiroz" %%% "scala-java-time-tzdb" % Versions.nativeScalaJavaTime % Test
)
)
}
)
//.enablePlugins(spray.boilerplate.BoilerplatePlugin)

lazy val files: ProjectMatrix = (projectMatrix in file("files"))
Expand All @@ -458,6 +469,7 @@ lazy val files: ProjectMatrix = (projectMatrix in file("files"))
)
.jvmPlatform(scalaVersions = scala2And3Versions)
.jsPlatform(scalaVersions = scala2And3Versions)
.nativePlatform(scalaVersions = List(scala3))
.dependsOn(core)

lazy val testing: ProjectMatrix = (projectMatrix in file("testing"))
Expand All @@ -471,6 +483,7 @@ lazy val testing: ProjectMatrix = (projectMatrix in file("testing"))
)
.jvmPlatform(scalaVersions = scala2And3Versions)
.jsPlatform(scalaVersions = scala2And3Versions, settings = commonJsSettings)
.nativePlatform(scalaVersions = List(scala3), settings = commonNativeSettings)
.dependsOn(core, circeJson % Test)

lazy val tests: ProjectMatrix = (projectMatrix in file("tests"))
Expand Down Expand Up @@ -505,11 +518,11 @@ lazy val perfTests: ProjectMatrix = (projectMatrix in file("perf-tests"))
name := "tapir-perf-tests",
libraryDependencies ++= Seq(
// Required to force newer jackson in Pekko, a version that is compatible with Gatling's Jackson dependency
"io.gatling.highcharts" % "gatling-charts-highcharts" % "3.11.4" % "test" exclude (
"io.gatling.highcharts" % "gatling-charts-highcharts" % "3.11.5" % "test" exclude (
"com.fasterxml.jackson.core",
"jackson-databind"
),
"io.gatling" % "gatling-test-framework" % "3.11.4" % "test" exclude ("com.fasterxml.jackson.core", "jackson-databind"),
"io.gatling" % "gatling-test-framework" % "3.11.5" % "test" exclude ("com.fasterxml.jackson.core", "jackson-databind"),
"com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.17.1",
"nl.grons" %% "metrics4-scala" % Versions.metrics4Scala % Test,
"com.lihaoyi" %% "scalatags" % Versions.scalaTags % Test,
Expand Down Expand Up @@ -575,6 +588,14 @@ lazy val cats: ProjectMatrix = (projectMatrix in file("integrations/cats"))
)
)
)
.nativePlatform(
scalaVersions = List(scala3),
settings = commonNativeSettings ++ Seq(
libraryDependencies ++= Seq(
"io.github.cquiroz" %%% "scala-java-time" % Versions.jsScalaJavaTime % Test
)
)
)
.dependsOn(core)

lazy val catsEffect: ProjectMatrix = (projectMatrix in file("integrations/cats-effect"))
Expand Down Expand Up @@ -760,6 +781,10 @@ lazy val circeJson: ProjectMatrix = (projectMatrix in file("json/circe"))
scalaVersions = scala2And3Versions,
settings = commonJsSettings
)
.nativePlatform(
scalaVersions = List(scala3),
settings = commonNativeSettings
)
.dependsOn(core)

lazy val json4s: ProjectMatrix = (projectMatrix in file("json/json4s"))
Expand Down Expand Up @@ -853,6 +878,14 @@ lazy val uPickleJson: ProjectMatrix = (projectMatrix in file("json/upickle"))
)
)
)
.nativePlatform(
scalaVersions = List(scala3),
settings = commonNativeSettings ++ Seq(
libraryDependencies ++= Seq(
"io.github.cquiroz" %%% "scala-java-time" % Versions.nativeScalaJavaTime % Test
)
)
)
.dependsOn(core)

lazy val picklerJson: ProjectMatrix = (projectMatrix in file("json/pickler"))
Expand Down Expand Up @@ -887,8 +920,8 @@ lazy val jsoniterScala: ProjectMatrix = (projectMatrix in file("json/jsoniter"))
.settings(
name := "tapir-jsoniter-scala",
libraryDependencies ++= Seq(
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.30.1",
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.30.1" % Test,
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.30.3",
"com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.30.3" % Test,
scalaTest.value % Test
)
)
Expand All @@ -897,6 +930,10 @@ lazy val jsoniterScala: ProjectMatrix = (projectMatrix in file("json/jsoniter"))
scalaVersions = scala2And3Versions,
settings = commonJsSettings
)
.nativePlatform(
scalaVersions = List(scala3),
settings = commonNativeSettings
)
.dependsOn(core)

lazy val zioJson: ProjectMatrix = (projectMatrix in file("json/zio"))
Expand Down Expand Up @@ -968,7 +1005,7 @@ lazy val pekkoGrpcExamples: ProjectMatrix = (projectMatrix in file("grpc/pekko-e
.settings(
name := "tapir-pekko-grpc-examples",
libraryDependencies ++= Seq(
"org.apache.pekko" %% "pekko-discovery" % "1.0.2",
"org.apache.pekko" %% "pekko-discovery" % "1.0.3",
slf4j
),
fork := true
Expand Down Expand Up @@ -1157,6 +1194,7 @@ lazy val serverCore: ProjectMatrix = (projectMatrix in file("server/core"))
.dependsOn(core % CompileAndTest)
.jvmPlatform(scalaVersions = scala2And3Versions, settings = commonJvmSettings)
.jsPlatform(scalaVersions = scala2And3Versions, settings = commonJsSettings)
.nativePlatform(scalaVersions = List(scala3), settings = commonNativeSettings)

lazy val serverTests: ProjectMatrix = (projectMatrix in file("server/tests"))
.settings(commonJvmSettings)
Expand Down Expand Up @@ -1829,6 +1867,7 @@ lazy val clientCore: ProjectMatrix = (projectMatrix in file("client/core"))
)
.jvmPlatform(scalaVersions = scala2And3Versions)
.jsPlatform(scalaVersions = scala2And3Versions)
.nativePlatform(scalaVersions = List(scala3))
.dependsOn(core)

lazy val http4sClient: ProjectMatrix = (projectMatrix in file("client/http4s-client"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,24 @@ private[tapir] object ErasureSameAsTypeMacros {

val t = TypeRepr.of[T]

if t.<:<(TypeRepr.of[reflect.Enum]) && t.typeSymbol.declaredFields.isEmpty then
report.errorAndAbort(
s"Type ${t.show} is a parameterless enum case. Such cases do not have a distinct run-time class, and hence cannot" +
s"be correctly used in a oneOfVariant. Consider using other matchers, such as oneOfVariantExactMatcher."
)

// substitute for `t =:= t.erasure` - https://github.com/lampepfl/dotty-feature-requests/issues/209
val isAllowed: TypeRepr => Boolean = {
case AppliedType(t, _) if t.typeSymbol.name == "Array" => true
case _: AppliedType | _: AndOrType => false
case _ => true
}

if (!isAllowed(t)) {
if !isAllowed(t) then
report.errorAndAbort(
s"Type ${t.show}, $t is not the same as its erasure. Using a runtime-class-based check it won't be possible to verify " +
s"that the input matches the desired type. Use other methods to match the input to the appropriate variant " +
s"instead."
)
}
}
}
38 changes: 37 additions & 1 deletion core/src/main/scala/sttp/tapir/Tapir.scala
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,8 @@ trait Tapir extends TapirExtensions with TapirComputedInputs with TapirStaticCon
): OneOfVariant[T] =
OneOfVariant(statusCode(code).and(output), matcher.lift.andThen(_.getOrElse(false)))

/** Create a one-of-variant which `output` if the provided value exactly matches one of the values provided in the second argument list.
/** Create a one-of-variant which uses `output` if the provided value exactly matches one of the values provided in the second argument
* list.
*
* Should be used in [[oneOf]] output descriptions.
*/
Expand All @@ -361,6 +362,41 @@ trait Tapir extends TapirExtensions with TapirComputedInputs with TapirStaticCon
): OneOfVariant[T] =
oneOfVariantValueMatcher(code, output)(exactMatch(rest.toSet + firstExactValue))

/** Create a one-of-variant which uses `output` if the provided value equals the singleton value. The `output` shouldn't map to any
* values, that is, it should be `Unit`-typed. The entire variant is, on the other hand, typed with the singleton's type `T`.
*
* Should be used in [[oneOf]] output descriptions.
*
* @see
* [[oneOfVariantExactMatcher]] which allows specifying more exact-match values, and where `output` needs to correspond to type `T`.
*/
def oneOfVariantSingletonMatcher[T](output: EndpointOutput[Unit])(singletonValue: T): OneOfVariant[T] =
oneOfVariantValueMatcher(output.and(emptyOutputAs(singletonValue)))({ case a: Any => a == singletonValue })

/** Create a one-of-variant which uses `output` if the provided value equals the singleton value. The `output` shouldn't map to any
* values, that is, it should be `Unit`-typed. The entire variant is, on the other hand, typed with the singleton's type `T`.
*
* Adds a fixed status-code output with the given value.
*
* Should be used in [[oneOf]] output descriptions.
*
* @see
* [[oneOfVariantExactMatcher]] which allows specifying more exact-match values, and where `output` needs to correspond to type `T`.
*/
def oneOfVariantSingletonMatcher[T](code: StatusCode, output: EndpointOutput[Unit])(singletonValue: T): OneOfVariant[T] =
oneOfVariantValueMatcher(code, output.and(emptyOutputAs(singletonValue)))({ case a: Any => a == singletonValue })

/** Create a one-of-variant which will use a fixed status-code output with the given value, if the provided value equals the singleton
* value. The entire variant is typed with the singleton's type `T`.
*
* Should be used in [[oneOf]] output descriptions.
*
* @see
* [[oneOfVariantExactMatcher]] which allows specifying more exact-match values, and where `output` needs to correspond to type `T`.
*/
def oneOfVariantSingletonMatcher[T](code: StatusCode)(singletonValue: T): OneOfVariant[T] =
oneOfVariantValueMatcher(code, emptyOutputAs(singletonValue))({ case a: Any => a == singletonValue })

/** Create a one-of-variant which uses `output` if the provided value matches the target type, as checked by [[MatchType]]. Instances of
* [[MatchType]] are automatically derived and recursively check that classes of all fields match, to bypass issues caused by type
* erasure.
Expand Down
55 changes: 55 additions & 0 deletions core/src/test/scala-3/sttp/tapir/EndpointScala3Test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package sttp.tapir

import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

class EndpointScala3Test extends AnyFlatSpec with Matchers:
import EndpointScala3Test.*

"oneOfVariant" should "compile when using parametrised enums" in {
endpoint.get
.out(
sttp.tapir.oneOf(
oneOfVariant(header[String]("X").mapTo[ParametrisedEnum.Case1]),
oneOfVariant(header[Int]("Y").mapTo[ParametrisedEnum.Case2])
)
)
}

it should "compile when using parameterless sealed traits" in {
endpoint.get
.out(
sttp.tapir.oneOf(
oneOfVariant(emptyOutputAs(ParameterlessSealed.Case1)),
oneOfVariant(emptyOutputAs(ParameterlessSealed.Case1))
)
)
}

// parameterless enums aren't translated to run-time classes, so the run-time checks will "glue" any parameterless
// instance to the first variant corresponding to a parameterless case
it should "not compile when using parameterless enums" in {
assertDoesNotCompile("""
endpoint.get
.out(
sttp.tapir.oneOf(
oneOfVariant(emptyOutputAs(ParameterlessEnum.Case1)),
oneOfVariant(emptyOutputAs(ParameterlessEnum.Case1))
)
)
""")
}

object EndpointScala3Test:
enum ParametrisedEnum:
case Case1(v: String)
case Case2(n: Int)

enum ParameterlessEnum:
case Case1
case Case2

sealed trait ParameterlessSealed
object ParameterlessSealed:
case object Case1 extends ParameterlessSealed
case object Case2 extends ParameterlessSealed
8 changes: 8 additions & 0 deletions doc/endpoint/oneof.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ val baseEndpoint = endpoint.errorOut(
)
```

### One-of-variant and singleton types

One-of variants can also be created so that they are used only for specific values. This is a specialisation of the
`oneOfVariantValueMatcher` methods, which allows for a more convenient and compact description.

There are two methods which allows working with multiple or single specific values: `oneOfVariantExactMatcher` and
`oneOfVariantSingletonMatcher`.

### Error outputs

Error outputs can be extended with new variants, which is especially useful for partial server endpoints, when the
Expand Down
12 changes: 11 additions & 1 deletion doc/generator/sbt-openapi-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ openapiUseHeadTagForObjectName false If tr
openapiJsonSerdeLib circe The json serde library to use.
openapiValidateNonDiscriminatedOneOfs true Whether to fail if variants of a oneOf without a discriminator cannot be disambiguated.
openapiMaxSchemasPerFile 400 Maximum number of schemas to generate in a single file (tweak if hitting javac class size limits).
openapiAdditionalPackages Nil Additional packageName/swaggerFile pairs for generating from multiple schemas
===================================== ==================================== ==================================================================================================
```

Expand Down Expand Up @@ -88,6 +89,16 @@ If `openapiUseHeadTagForObjectName = true`, then the `GET /foo` and `GET /bar`
`Baz.scala` file, containing a single `object Baz` with those endpoint definitions; the `PUT /foo` endpoint, by dint of
having no tags, would be output to the `TapirGeneratedEndpoints` file, along with any schema and parameter definitions.

Files can be generated from multiple openapi schemas if `openapiAdditionalPackages` is configured; for example
```sbt
openapiAdditionalPackages := List(
"sttp.tapir.generated.v1" -> baseDirectory.value / "src" / "main" / "resources" / "openapi_v1.yml")
```
would generate files in the package `sttp.tapir.generated.v1` based on the `openapi_v1.yml` schema at the provided
location. This would be in addition to files generated in `openapiPackage` from the specs configured by
`openapiSwaggerFile`


### Json Support

```{eval-rst}
Expand All @@ -106,7 +117,6 @@ jsoniter "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala
Currently, string-like enums in Scala 2 depend upon the enumeratum library (`"com.beachape" %% "enumeratum"`).
For Scala 3 we derive native enums, and depend on `"io.github.bishabosha" %% "enum-extensions"` for generating query
param serdes.
Other forms of OpenApi enum are not currently supported.

Models containing binary data cannot be re-used between json and multi-part form endpoints, due to having different
representation types for the binary data
Expand Down
2 changes: 2 additions & 0 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ We offer commercial support for sttp and related technologies, as well as develo
tutorials/02_openapi_docs
tutorials/03_json
tutorials/04_errors
tutorials/05_multiple_inputs_outputs
tutorials/06_error_variants
.. toctree::
:maxdepth: 2
Expand Down
8 changes: 0 additions & 8 deletions doc/server/ziohttp.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,6 @@ val countCharactersHttp: Routes[Any, Response] =
ZioHttpInterpreter().toHttp(countCharactersEndpoint.zServerLogic(countCharacters))
```

```{note}
A single ZIO-Http application can contain both tapir-managed and ZIO-Http-managed routes. However, because of the
routing implementation in ZIO Http, the shape of the paths that tapir and other ZIO Http handlers serve should not
overlap. The shape of the path includes exact path segments, single- and multi-wildcards. Otherwise, request handling
will throw an exception. We don't expect users to encounter this as a problem, however the implementation here
diverges a bit comparing to other interpreters.
```

## Server logic

When defining the business logic for an endpoint, the following methods are available, which replace the
Expand Down
4 changes: 4 additions & 0 deletions doc/tutorials/04_errors.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# 4. Error handling

```{note}
The tutorial is also available [as a video](https://www.youtube.com/watch?v=iXGJsk4_2Dg).
```

Many things can go wrong: that's why error handling is often the centerpiece of software libraries. We got a glimpse of
one of Tapir's components when it comes to error handling when we discussed
[adding OpenAPI documentation](02_openapi_docs.md). In this tutorial, we'll investigate Tapir's approach to error
Expand Down
Loading

0 comments on commit 50919d2

Please sign in to comment.