Giter VIP home page Giter VIP logo

softwaremill / tapir Goto Github PK

View Code? Open in Web Editor NEW
1.3K 48.0 392.0 17.3 MB

Declarative, type-safe web endpoints library

Home Page: https://tapir.softwaremill.com

License: Apache License 2.0

Scala 99.69% Makefile 0.02% Python 0.19% Batchfile 0.03% Shell 0.01% HTML 0.01% JavaScript 0.01% TypeScript 0.02% Nix 0.03%
akka-http openapi http4s sttp http scala declarative type-safe functional-programming akka

tapir's Introduction

tapir

Welcome!

Ideas, suggestions, problems, questions CI Maven Central

Intro

With tapir, you can describe HTTP API endpoints as immutable Scala values. Each endpoint can contain a number of input and output parameters. An endpoint specification can be interpreted as:

  • a server, given the "business logic": a function, which computes output parameters based on input parameters. Currently supported:
  • a client, which is a function from input parameters to output parameters. Currently supported:
  • documentation. Currently supported:

Depending on how you prefer to explore the library, take a look at one of the examples or head over to the docs for a more detailed description of how tapir works! Or, use adopt-tapir to generate a tapir-based project in a couple of clicks! You can also generate a stub of a tapir-based application directly from the command line with sbt new softwaremill/tapir.g8. Finally, ScalaDocs are available at javadoc.io.

Why tapir?

  • type-safety: compile-time guarantees, develop-time completions, read-time information
  • declarative: separate the shape of the endpoint (the "what"), from the server logic (the "how")
  • OpenAPI / Swagger integration: generate documentation from endpoint descriptions
  • observability: leverage the metadata to report rich metrics and tracing information
  • abstraction: re-use common endpoint definitions, as well as individual inputs/outputs
  • library, not a framework: integrates with your stack

Adopters

Is your company already using tapir? We're continually expanding the "adopters" section in the documentation; the more the merrier! It would be great to feature your company's logo, but in order to do that, we'll need written permission to avoid any legal misunderstandings.

Please email us at [email protected] from your company's email with a link to your logo (if we can use it, of course!) or with details who to kindly ask for permission to feature the logo in tapir's documentation. We'll handle the rest.

Adobe Swisscom Swissborg
Kaizo Process Street Tranzzo
Kelkoo group SoftwareMill Carvana
Moneyfarm Ocado Technology Wegtam
Broad Kensu Colisweb
iceo dpg hunters
moia hootsuite

Teaser

import sttp.tapir._
import sttp.tapir.generic.auto._
import sttp.tapir.json.circe._
import io.circe.generic.auto._

type Limit = Int
type AuthToken = String
case class BooksQuery(genre: String, year: Int)
case class Book(title: String)


// Define an endpoint

val booksListing: PublicEndpoint[(BooksQuery, Limit, AuthToken), String, List[Book], Any] = 
  endpoint
    .get
    .in(("books" / path[String]("genre") / path[Int]("year")).mapTo[BooksQuery])
    .in(query[Limit]("limit").description("Maximum number of books to retrieve"))
    .in(header[AuthToken]("X-Auth-Token"))
    .errorOut(stringBody)
    .out(jsonBody[List[Book]])


// Generate OpenAPI documentation

import sttp.apispec.openapi.circe.yaml._
import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter

val docs = OpenAPIDocsInterpreter().toOpenAPI(booksListing, "My Bookshop", "1.0")
println(docs.toYaml)


// Convert to akka-http Route

import sttp.tapir.server.akkahttp.AkkaHttpServerInterpreter
import akka.http.scaladsl.server.Route
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

def bookListingLogic(bfy: BooksQuery,
                     limit: Limit,
                     at: AuthToken): Future[Either[String, List[Book]]] =
  Future.successful(Right(List(Book("The Sorrows of Young Werther"))))
  
val booksListingRoute: Route = AkkaHttpServerInterpreter()
  .toRoute(booksListing.serverLogic((bookListingLogic _).tupled))


// Convert to sttp Request

import sttp.tapir.client.sttp.SttpClientInterpreter
import sttp.client3._

val booksListingRequest: Request[DecodeResult[Either[String, List[Book]]], Any] = 
  SttpClientInterpreter()
    .toRequest(booksListing, Some(uri"http://localhost:8080"))
    .apply((BooksQuery("SF", 2016), 20, "xyz-abc-123"))

Documentation

tapir documentation is available at tapir.softwaremill.com.

Quickstart with sbt

Add the following dependency:

"com.softwaremill.sttp.tapir" %% "tapir-core" % "1.10.5"

Then, import:

import sttp.tapir._

And finally, type endpoint. and see where auto-complete gets you!

Scala 2.12

Partial unification is now enabled by default from Scala 2.13. However, if you're using Scala 2.12 or older, then you'll need partial unification enabled in the compiler (alternatively, you'll need to manually provide type arguments in some cases):

scalacOptions += "-Ypartial-unification"

Sidenote for scala 2.12.4 and higher: if you encounter an issue with compiling your project because of a StackOverflowException related to this scala bug, please increase your stack memory. Example:

sbt -J-Xss4M clean compile

Other sttp projects

sttp is a family of Scala HTTP-related projects, and currently includes:

  • sttp client: the Scala HTTP client you always wanted!
  • sttp tapir: this project
  • sttp model: simple HTTP model classes (used by client & tapir)
  • sttp shared: shared web socket, FP abstractions, capabilities and streaming code.
  • sttp apispec: OpenAPI, AsyncAPI and JSON Schema models.

Contributing

All suggestions welcome :)

See the list of issues and pick one! Or report your own.

If you are having doubts on the why or how something works, don't hesitate to ask a question on discourse or via github. This probably means that the documentation, scaladocs or code is unclear and be improved for the benefit of all. In order to develop the documentation, you can use the doc/watch.sh script, which runs Sphinx using Python. Use doc/requirements.txt to set up your Python environment with pip. If you're using Nix, you can just run nix develop in the doc directory to set up a working shell with all the dependencies.

The core module needs to remain binary-compatible with earlier versions. To check if your changes meet this requirement, you can run core/mimaReportBinaryIssues from the sbt console. However, be aware that tags from the repository aren’t automatically fetched during forking; hence, the command will not operate without first fetching the tags.

After forking and cloning the repository, add the original repository as a remote:

git remote add upstream [email protected]:softwaremill/tapir.git

Fetch the tags from the upstream:

git fetch --tags upstream

Scoping which projects are included by sbt

  • when STTP_NATIVE is set, Scala native projects are included in the build (when running sbt)
  • when ALSO_LOOM is set, projects using virtual threads and requiring Java 21 are included in the build
  • when ONLY_LOOM is set, only projects using virtual threads are included in the build

Testing locally

The JS tests use Gecko instead of Chrome, although this causes another problem: out of memory when running JS tests for multiple modules. Work-arounds:

  • run only tests for a specific Scala version and platform using testScoped 2.13 JS (supported versions: 2.12, 2.13, 3; supported platforms: JVM, JS, Native)
  • test single JS projects
  • use CI (GitHub Actions) to test all projects - the .github/workflows/ci.yml enumerates them one by one

You can test only server/client/doc/other projects using testServers, testClients, testDocs and testOther.

To verify that the code snippet in docs compile, run compileDocumentation. A full mdoc run is done during a release (when the documentation is generated).

Importing into IntelliJ

By default, when importing to IntelliJ, only the Scala 3/JVM subprojects will be imported. This is controlled by the ideSkipProject setting in build.sbt (inside commonSettings).

If you'd like to work on a different platform or Scala version, simply change this setting temporarily so that the correct subprojects are imported. For example:

// import only Scala 2.13, JS projects
ideSkipProject := (scalaVersion.value != scala2_13) || !thisProjectRef.value.project.contains("JS")

// import only Scala 3, JVM projects
ideSkipProject := (scalaVersion.value != scala3) || thisProjectRef.value.project.contains("JS") || thisProjectRef.value.project.contains("Native"),

// import only Scala 2.13, Native projects
ideSkipProject := (scalaVersion.value != scala2_13) || !thisProjectRef.value.project.contains("Native")

Commercial Support

We offer commercial support for tapir and related technologies, as well as development services. Contact us to learn more about our offer!

Copyright

Copyright (C) 2018-2024 SoftwareMill https://softwaremill.com.

tapir's People

Contributors

adamw avatar aesteve avatar danielleontiev avatar dvgica avatar felix-lipski avatar frekw avatar ghostbuster91 avatar ghostdogpr avatar gzhk avatar hughsimpson avatar ikhoon avatar kciesielski avatar kubinio123 avatar lbialy avatar marcin-jozefowicz avatar matwojcik avatar mbore avatar mergify[bot] avatar micossow avatar mkrzemien avatar mszczygiel avatar pask423 avatar pewniak747 avatar pierscin avatar rafalambrozewicz avatar rucek avatar sameerparekh avatar scala-steward avatar slabiakt avatar softwaremill-ci avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tapir's Issues

DecodeFailureHandler should optionally return error values

The DecodeFailureHandler should be extended with a type parameter: DecodeFailureHandler[+E], so that for a decode failure, it can either directly complete a request, return a no match, or (which is new) an error value, which will then be handled as if the server logic returned that error value.

The default failure handler can have the type DecodeFailureHandler[Nothing] (hence the variance)

Error Handling & status codes - default and route specific

I'd like to have the 'default' error types defined once and to be able to handle exceptions and return those error types.

i.e. have one place that defines: 400, 401, 404, 409, etc. with a default message (i.e. 401 Not Authorized)
and if there is a specific error for a specific route - add its own error type and doc, and be able to override an already existing status code.
i.e. in the specific route - 401 "You don't have permissions to modify the book store"

Handling of value classes

I'm using value classes to have a type-safe representation of e.g. IDs.

For example, I have this case class:

case class UserId(value: String) extends AnyVal

With the help of io.circe.generic.extras.deriveUnwrappedEncoder this is represented in JSON as "id": "1" instead of "id": {"value": "1"}.

How can I make tapir aware of this situation. It currently outputs this OpenAPI doc:

id:
  required:
  - value
  type: object
  properties:
    value:
      type: string

instead of:

id:
  type: string

Support for multiple response codes

Currently all responses end with 200 or 400 response codes. When creating a server, this should be customisable. Maybe the endpoint should hold classifiers E => StatusCode, O => StatusCode, where: Endpoint[I, E, O]?

Runtime error when there is no schema for object

Given following enum:

    sealed trait EntityType
    case object Person extends EntityType
    case object Org extends EntityType

and endpoint:

  val e = endpoint.post
    .out(jsonBody[List[EntityType]])

actual behavior:

Exception in thread "main" java.util.NoSuchElementException: key not found: SObjectInfo(sakiewka.local.WalletApi.Response.User,List())
	at scala.collection.MapLike.default(MapLike.scala:235)
	at scala.collection.MapLike.default$(MapLike.scala:234)
	at scala.collection.AbstractMap.default(Map.scala:63)
	at scala.collection.MapLike.apply(MapLike.scala:144)
	at scala.collection.MapLike.apply$(MapLike.scala:143)
	at scala.collection.AbstractMap.apply(Map.scala:63)
	at tapir.docs.openapi.schema.SchemaReferenceMapper.map(SchemaReferenceMapper.scala:8)
	at tapir.docs.openapi.schema.TSchemaToOSchema$$anonfun$apply$2.applyOrElse(TSchemaToOSchema.scala:49)
	at tapir.docs.openapi.schema.TSchemaToOSchema$$anonfun$apply$2.applyOrElse(TSchemaToOSchema.scala:49)
	at scala.PartialFunction.$anonfun$runWith$1$adapted(PartialFunction.scala:145)
	at scala.collection.immutable.Set$Set3.foreach(Set.scala:169)
	at scala.collection.TraversableLike.collect(TraversableLike.scala:274)
	at scala.collection.TraversableLike.collect$(TraversableLike.scala:272)
	at scala.collection.AbstractTraversable.collect(Traversable.scala:108)
	at tapir.docs.openapi.schema.TSchemaToOSchema.apply(TSchemaToOSchema.scala:49)
	at tapir.docs.openapi.schema.TSchemaToOSchema.apply(TSchemaToOSchema.scala:35)
	at tapir.docs.openapi.schema.ObjectSchemas.apply(ObjectSchemas.scala:17)
	at tapir.docs.openapi.CodecToMediaType.apply(CodecToMediaType.scala:18)
	at tapir.docs.openapi.EndpointToOperationResponse$$anonfun$3.applyOrElse(EndpointToOperationResponse.scala:80)
	at tapir.docs.openapi.EndpointToOperationResponse$$anonfun$3.applyOrElse(EndpointToOperationResponse.scala:79)
	at scala.PartialFunction.$anonfun$runWith$1$adapted(PartialFunction.scala:145)
	at scala.collection.Iterator.foreach(Iterator.scala:941)
	at scala.collection.Iterator.foreach$(Iterator.scala:941)
	at scala.collection.AbstractIterator.foreach(Iterator.scala:1429)
	at scala.collection.IterableLike.foreach(IterableLike.scala:74)
	at scala.collection.IterableLike.foreach$(IterableLike.scala:73)
	at scala.collection.AbstractIterable.foreach(Iterable.scala:56)
	at scala.collection.TraversableLike.collect(TraversableLike.scala:274)
	at scala.collection.TraversableLike.collect$(TraversableLike.scala:272)
	at scala.collection.AbstractTraversable.collect(Traversable.scala:108)
	at tapir.docs.openapi.EndpointToOperationResponse.outputToResponse(EndpointToOperationResponse.scala:79)
	at tapir.docs.openapi.EndpointToOperationResponse.outputToResponses(EndpointToOperationResponse.scala:43)
	at tapir.docs.openapi.EndpointToOperationResponse.apply(EndpointToOperationResponse.scala:15)
	at tapir.docs.openapi.EndpointToOpenApiPaths.endpointToOperation(EndpointToOpenApiPaths.scala:54)
	at tapir.docs.openapi.EndpointToOpenApiPaths.pathItem(EndpointToOpenApiPaths.scala:32)
	at tapir.docs.openapi.EndpointToOpenAPIDocs$.$anonfun$toOpenAPI$2(EndpointToOpenAPIDocs.scala:19)
	at scala.collection.TraversableLike.$anonfun$map$1(TraversableLike.scala:237)
	at scala.collection.immutable.List.foreach(List.scala:392)
	at scala.collection.TraversableLike.map(TraversableLike.scala:237)
	at scala.collection.TraversableLike.map$(TraversableLike.scala:230)
	at scala.collection.immutable.List.map(List.scala:298)
	at tapir.docs.openapi.EndpointToOpenAPIDocs$.toOpenAPI(EndpointToOpenAPIDocs.scala:19)
	at tapir.docs.openapi.TapirOpenAPIDocs$RichOpenAPIEndpoints.toOpenAPI(TapirOpenAPIDocs.scala:18)
	at tapir.docs.openapi.TapirOpenAPIDocs$RichOpenAPIEndpoints.toOpenAPI(TapirOpenAPIDocs.scala:15)

expected behavior:
It should work :)

Also please notice that when specifying endpoint like that:

  val e = endpoint.post
    .out(jsonBody[EntityType])

everything works fine.

I also noticed that when Entity is specified as follow:

 sealed trait EntityType{
    case object Person extends EntityType
    case object Org extends EntityType
}

There is a compilation error about not being able to find schemaFor Entity

Investigate stackoverflow when dealing with tree structure

For structure as in #84 user should be able to simply write:

  def sEdge:SchemaFor[Edge] = implicitly[SchemaFor[Edge]]
  def sSimpleNode:SchemaFor[SimpleNode] = implicitly[SchemaFor[SimpleNode]]

  implicit val sNode: SchemaFor[Node] =
    SchemaFor.oneOf[Node, String](_.kind, _.toString)(
      "Edge" -> sEdge,
      "SimpleNode" -> sSimpleNode
    )

Unfortunately this code produces stackoverflow exception.

Improve api consistency

I wonder if it is a good thing that Endpoint has so flexible api, e.g. following case are equivalent:

val booksListing: Endpoint[Limit, String, Vector[Book], Nothing] = baseEndpoint.get
    .in("list" / "all")
    .in(limitParameter)
    .out(jsonBody[Vector[Book]])
val booksListing: Endpoint[Limit, String, Vector[Book], Nothing] = baseEndpoint.get
    .in("list" / "all" and limitParameter)
    .out(jsonBody[Vector[Book]])
val booksListing: Endpoint[Limit, String, Vector[Book], Nothing] = baseEndpoint.get
    .in("list" / "all" / limitParameter)
    .out(jsonBody[Vector[Book]])
val booksListing: Endpoint[Limit, String, Vector[Book], Nothing] = baseEndpoint.get
    .in("list" / "all" & limitParameter)
    .out(jsonBody[Vector[Book]])

where limitParameter is definied as follow:

private val limitParameter = query[Option[Int]]("limit")
        .description("Maximum number of books to retrieve")

What's even worse, somebody can just type:

val booksListing: Endpoint[(Limit,Limit) String, Vector[Book], Nothing] = baseEndpoint.get
    .in("list" / "all" & limitParameter / "something" / limitParamter)
    .out(jsonBody[Vector[Book]])

which is a pure abomination :)

Doesn't such ambiguity make adoption slower?

In opposite to that I was thinking about something like this:

val booksListing: Endpoint[Limit, String, Vector[Book], Nothing] = baseEndpoint.get
    .in("list" / "all" ? limitParameter & otherQueryParam)
    .out(jsonBody[Vector[Book]])

Where "list" and "all" are both PathParams and only they have this / method to concatenate with other pathParams. Also there is ? method defined on them which allows joining with queryParams and it changes the object type to queryParam so there is no option to add next pathParams anymore (which in contrary is possible with current api).

There are still some things which are unclear with proposed change like:

  • how to concatenate with headers? and other types
  • should we allow specyfing EndpointInput multiple times?

I made a trashy implementation to better visualize my idea.

From it it seems that it is not a good approach due to sealed traits explosion and duplication.

Maybe it would be better to change Endoint.in method to accept some kind of higher abstraction dsl which would be internally converted to old EndpointInput structure?

See e8795f7

All paths should start with /

As discussed in #23 , path matching should start with /:

endpoint.path(/) // matches only root (no path and /)
endpoint.path(/ "x" / "y" / "z") // matches only /x/y/z and /x/y/z/
endpoint.path(/ "x").path(/ "y" / "z")

Hence, / should be an "empty path" object, and a method to combine two paths

Monitor files

Quickpar has the function to monitor files when there are some files are missing or uncomplete. The application looks for new files from time to time and updates the file list. I miss this option in multipar? is there something like this or can it be implemented?

Add authentication/authorization support

OpenAPI already has the authentication spec so that it'd be nice to be able to bake it into the tapir endpoints.

Currently TSec seems like the most modular and feature-rich project in this regard, so that I'm interested in combining a tapir endpoint with an auth spec and tsec in the server side.

TSec already has tsec-http4s module. To support both akka-http and http4s, we can:

  1. Factor out some common components outside of tsec-http4s and then add something like tsec-akka-http on TSec side.
  2. Make such common module in tapir depending on tsec-core from scratch.
  3. Just use tsec-http4s as it is, and make another subproject for akka-http.

Scanning for object schemas doesn't go deep enough

There are two cases where the objects are not scanned:

  1. When TSchema.SCoproduct contains a complex object - only the object itself is added - there is no recursive call to the fields
  2. case TSchema.SArray(o: TSchema.SCoproduct) is not covered - it misses the objects that are defined with oneOf if they are in TSchema.SArray

Failing tests on travis

Occasionally tests fail on travis due to binding exception.
It affects both akka and http4s tests. I came up with a solution to restart test if the exception was BindException but for unknown reasons to me akkaHttp wraps exception with akka.stream.impl.io.ConnectionSourceStage$$anon$1$$anon$2. I could workaround it catching both exceptions within recoverWith section, or apply this solution separately to both implementations, neither of those options satisfy me.

My solution was (this one works only for akka):

  def testServer(name: String, rs: => NonEmptyList[ROUTE])(runTest: Uri => IO[Assertion]): Unit = {
    val resources = for {
      port <- Resource.liftF(IO(nextPort()))
      _ <- server(rs, port)
    } yield uri"http://localhost:$port"
    test(name)(restartOnBindException(resources).use(runTest).unsafeRunSync())
  }

  private def restartOnBindException(r: Resource[IO, Uri]): Resource[IO, Uri] =
    r.recoverWith { case e if e.getCause.isInstanceOf[BindException] => restartOnBindException(r) }

Might it be related with something like travis-ci/travis-ci#8089?

Streaming support

Maybe using the same mechanism as in sttp - an additional type parameter specifying requirements for the interpreter?

Possible syntax: streamBody[S] (as in sttp). This would allow capturing the requirement for a specific stream support.

Support for SSE and websockets

Tapir provides support for Akka Http routes and writing client for that, but it would be great if there is a support for Server-sent events (SSE) and websockets at server and client level as Akka Http supports both of them.

Define schemas for maps

The default schema for Map should be an object without arbitrary fields. Alternatively: only for Map[String, V] for any V.

Such a schema should also be properly generated in the docs.

It is impossible to reference to a SCoproduct

I have a complex API structure - in the form of a Graph. the nodes are recursive.

this means that I need to use SRef to avoid cyclic definitions of schemas.
the problem is that I cannot use an SRef to reference a SCoproduct - since SCoproduct doesn't have its own name.

consider this structure for example:

sealed trait Node
case class Edge(id: Long, source: Node, target: Node) extends Node
case class SimpleNode(id: Long, data: List[Data]) extends Node

case class Data(attribute: Attr)

sealed trait Attr
case class SimpleAttr(name: String, value: String) extends Attr
case class MultiAttr(name: String, value: List[String]) extends Attr

defining implicit schemas for the above class would require me to use:

  implicit val sNode: SchemaFor[Node] =
    SchemaFor.oneOf[Node, String](_.kind, _.toString)(
      "Edge" -> sEdge,
      "SimpleNode" -> sSimpleNode
    ) 

and then I would need to reference it:

  implicit val sEdge: SchemaFor[Edge] =
    SchemaFor(SObject(Schema.SObjectInfo("Edge"), List(
      ("id", Schema.SNumber),
      ("source", SRef(SObjectInfo("Node"))),
      ("target", SRef(SObjectInfo("Node")))
    ), List("id", "source", "target")))

but this doesn't work - since there is no such referench for "Node" since it is a SCoproduct and doesn't have an ObjectInfo

Alternative Client API style

I'm not sure if it makes sense for this project, but in the past I've seen a (closed source) api client generator that generated calls that resembled the API url.

Example: Imagine an endpoint which has the url {host}/api/v1/profile/{profileId}/properties. A "get call" for that endpoint, in client code, would look like ExternalService.api.v1.profile(profileId).properties.get.

The two things that I really like about that was that the code resembled the url and that the IDE helped discover the API with autocompletion.

Is this kind of client something that you would consider?

Content-Type JSON is missing since version 0.7.7

Hi,

I've found some regression. Since version 0.7.7 Content-Type JSON is not present in response from generated routes (http4s).

Example response with Tapir 0.7.6:

HTTP/1.1 200 OK
Content-Length: 19
Content-Type: application/json
Date: Thu, 16 May 2019 13:41:35 GMT

{
    "x": "1 ABC", 
    "y": 6
}

Example response with Tapir 0.7.9 (the same for 0.7.8 and 0.7.7):

HTTP/1.1 200 OK
Content-Length: 19
Date: Thu, 16 May 2019 13:40:49 GMT

{"x":"1 ABC","y":6}

All-parameter inputs/outputs

Support inputs/outputs which capture all parameters of a given type:

  • allQueryParams: Seq[(String, String)] / queryParams
  • allHeaders: Seq[(String, String)] / headers
  • remainingPath: List[String]

All urlencoded-form parameters are already part of #2 (supporting the form data body type).

Support openapi/jsonschema value constraints

When interpreted as openapi docs, this would translate to the following properties from the schema object (https://swagger.io/specification/#schemaObject):

  • enum
  • minimum, maximum
  • pattern
  • maxLength/minLength

The tapir.Schema trait should be extended with a description of value constraints (default no constraints).

The encouraged (and documented) way to define constraints would be by creating new types. This could be through e.g. tagged types (which have no runtime overhead, e.g. https://github.com/softwaremill/scala-common#tagging), value types or "normal" custom types.

We should provide a convenient way to define constraints for a type - through builder methods on schema. Possible syntax:

implicit val schemaForEmail: SchemaFor[String @@ Email] = 
  SchemaFor[String].constraint(Pattern(emailRegex))

implicit val schemaForColors: SchemaFor[String @@ Color] = 
  SchemaFor[String].constraint(Enum("red", "pink", "violet"))

The constraints defined on the schema should also be checked during decoding - resulting in a new DecodeResult.InvalidValue (or maybe reuse/replace Mismatch)?

No type parameters found

I am running the example and occur errors like this:

Error:(80, 13) no type parameters for method toRoute: (logic: FN[scala.concurrent.Future[Either[String,Unit]]])(implicit paramsAsArgs: tapir.typelevel.ParamsAsArgs.Aux[(tapir.example.Book, tapir.example.Endpoints.AuthToken),FN], implicit serverOptions: tapir.server.akkahttp.AkkaHttpServerOptions, implicit statusMapper: tapir.server.StatusMapper[Unit], implicit errorStatusMapper: tapir.server.StatusMapper[String])akka.http.scaladsl.server.Route exist so that it can be applied to arguments ((tapir.example.Book, tapir.example.Endpoints.AuthToken) => scala.concurrent.Future[Either[String,Unit]]) --- because --- argument expression's type is not compatible with formal parameter type; found : (tapir.example.Book, tapir.example.Endpoints.AuthToken) => scala.concurrent.Future[Either[String,Unit]] (which expands to) (tapir.example.Book, String) => scala.concurrent.Future[Either[String,Unit]] required: ?FN[scala.concurrent.Future[Either[String,Unit]]] addBook.toRoute(bookAddLogic _) ~

Why this error? and how to solve?

Multipart support

New endpoint io: multipart[T](name), which yields/accepts a Part[T] value, which has the following parameters:

  • additional headers
  • optional filename
  • content

The part content can be any of the basic body types (string, byte array, file, etc.). Are streaming parts possible? Probably not.

Support for sealed trait families

Derive the schema correctly
Specify discriminators for documentation generation

Encoding/decoding is handled by user-provided circe encoders/decoders, so nothing to do in the client/server front

Status code mappings as part of the endpoint description

As suggested on gitter, it would be nice if there was a way to describe status code mappings. Currently, this is only available when interpreting as a server by providing implicit StatusMapper values for error and normal outputs; the openapi documentation always generates two responses: default (for errors) and 200 (for normal outputs). This can only be customised by hand after generating the openapi model.

It would be better if there was a way to capture status code mappings as part of the endpoint description.

Possible API, in Tapir.scala:

  def statusFrom[I](io: EndpointIO[I], default: StatusCode, when: (When[I], StatusCode)*): EndpointIO[I] = ???

  def whenClass[U: ClassTag: SchemaFor]: When[Any] = WhenClass(implicitly[ClassTag[U]], implicitly[SchemaFor[U]])
  def whenValue[U](p: U => Boolean): When[U] = WhenValue(p)

  trait When[-I] {
    def matches(i: I): Boolean
  }
  case class WhenClass[T](ct: ClassTag[T], s: SchemaFor[T]) extends When[Any] {
    override def matches(i: Any): Boolean = ct.runtimeClass.isInstance(i)
  }
  case class WhenValue[T](p: T => Boolean) extends When[T] {
    override def matches(i: T): Boolean = p(i)
  }

usage:

  import tapir._
  import tapir.json.circe._
  import io.circe.generic.auto._

  trait ErrorInfo {
    def z: Boolean
  }
  case class E1(x: String, z: Boolean) extends ErrorInfo
  case class E2(y: Int, z: Boolean) extends ErrorInfo

  implicit val c: CodecForOptional[ErrorInfo, MediaType.Json, _] = null

  val ee = endpoint.out(
    statusFrom(
      jsonBody[ErrorInfo],
      StatusCodes.InternalServerError,
      whenClass[E1] -> StatusCodes.NotFound,
      whenClass[E2] -> StatusCodes.BadRequest,
      whenValue[ErrorInfo](_.z) -> StatusCodes.AlreadyReported
    ))

The server would then be able to generate a proper response code basing on the output value. The openapi docs could contain different output body schemas for different status codes. The client interpreter here wouldn't do anything with this kind of metadata, as the only thing it could do is validate that the status code matches the value received.

Outstanding problems:

  1. for automatic codec derivation this relies on #10: node the null codec in the example above to make the code compile. If there are multiple status codes, this usually means multiple subclasses of a class representing errors.
  2. should statusFrom wrap any kind of input, or only bodies? I think it needs to be constrained to wrapping bodies only, so that we could require the schema of the subclasses, and use these schemas in documentation. This would mean we wouldn't support differentiating status codes on headers (which is the other output option)

Add support for scala 2.11

I tried to use tapir with scala 2.11 but cannot locate any public jar files for that version of Scala. Would you be able to support this?

Request handled by wrong route

when defining 2 routes: one with /xx/{id} (id defined as Long) and another with /xx/yy
the request to /xx/yy is handled by the route for /xx/{id}

the error I get for it is: Invalid value for: path
which is not very informative, I would rather also get the caused by - as only by debugging it I understood what was the bug.
in my case that would have been: numberFormatException on "export"

I have the following base endpoints: (they contain previous parts of the paths - 3 Ints as path param)

val notionEndpoint = baseEndpoint.in("notion")
val notionIdEndpoint = notionEndpoint.in(path[Long]("notionId"))

then I have some routes:

 val getNotion: ServerEndpoint[(Int, Int, Int, Long), Unit, Option[Notion], Nothing, Future] =
      notionIdEndpoint.get.summary("Get a single notion").tag("Notion")
        .out(jsonBody[Option[Notion]])
        .serverLogic((getNotionLogic _).tupled)

val export: ServerEndpoint[(Int, Int, Int, Boolean, Boolean), Unit, (String, Nodes), Nothing, Future] = {
      notionEndpoint.get
        .in("export")
        .in(query[Option[Boolean]]("includeX")
          .example(Some(true))
          .map(_.getOrElse(true))(Option.apply))
        .in(query[Option[Boolean]]("includeY")
          .example(Some(true))
          .map(_.getOrElse(true))(Option.apply))
        .out(header[String]("Content-Disposition"))
        .out(jsonBody[Nodes])
        .tag("Import Export")
        .serverLogic((exportLogic _).tupled)
    }

and lastly - I return a list of endpoints to be later transformed to routes:

List(getNotion, export)

workaround: manually set the order of the endpoints for the export to be first

Support for other body types

To be done:

  • body[Array[Byte]]
  • body[ByteBuffer]
  • body[InputStream]
  • body[File]: save server request body/client response to temporary file (in the future: configurable file storage); server response/client request: send the given file
  • body[Seq[(String, String)]] / body[Map[String, String]]: form data parameters (only for urlencoded forms, not multipart!)

GraphQL support

Hello! Intriguing project! :)
Are there any plans or thoughts to add GraphQL-server support?
Thank you!

Detailed comparison for the similar projects

Tapir is a really nice start! But there are already many other projects similar to this and it may be really hard to understand which one to choose and when. Would be nice to have a detailed comparison of the main competitors. :)

Support for form field bodies

New endpoint io: form[T](name) which:

  • mandates the body to be urlencoded form parameters
  • reads the given form data parameter

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.