softwaremill / tapir Goto Github PK
View Code? Open in Web Editor NEWRapid development of self-documenting APIs
Home Page: https://tapir.softwaremill.com
License: Apache License 2.0
Rapid development of self-documenting APIs
Home Page: https://tapir.softwaremill.com
License: Apache License 2.0
When deriving codecs for forms, the in-scope tapir.generic.Configuration
should be used to generate field names.
It would be a nice addition to be able to supply description
and externalDocs
for tags
. See https://swagger.io/docs/specification/grouping-operations-with-tags/.
By default, use UTF-8 (explicitly)
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?
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?
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:
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.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)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.
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
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
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
Implement a http4s interpreter
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?
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)
When interpreted as openapi docs, this would translate to the following properties from the schema object (https://swagger.io/specification/#schemaObject):
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
)?
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
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).
Would it be possible to generate play routes from an endpoint description?
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
e.g. /my/path/is/good
is matched by endpoint.get.in("my" / "path")
even though there is more to consume.
In the tapir.typelevel
package - auto-generate typeclasses for all arities 1-n (n = 8? 16? 22? - to practical limits).
Project is unable to compile if our model is annotated with Swagger annotations. It's failing on searching implicit for SchemaFor
.
I've prepare example test:
https://github.com/PawelJ-PL/tapir-test-annotation-implicits
When building a server, if the path of the request doesn't match the endpoint, a "not matching" result is returned (and the next route tried). However, if the body fails to parse, the result should probably be to return a failed response immediately.
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 filebody[Seq[(String, String)]]
/ body[Map[String, String]]
: form data parameters (only for urlencoded forms, not multipart!)... and use endpoints both in frontend and backend :)
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.
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.
Mainly in queries, but also in headers
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?
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:
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
Similarly to circe, schema derivation (which uses magnolia) should be configurable to generate camelCase or snake_case field names.
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?
Add support for finatra
Hello guys! Would you add enum support like in this thread https://stackoverflow.com/questions/27603871/how-to-define-enum-in-swagger-io ?
P.S. Thank you for such a great library.
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. :)
Thanks for this great library! I think it would be helpful to have the ability to set the servers
field using Tapir (https://swagger.io/docs/specification/api-host-and-base-path/). This would render a drop-down in Swagger UI which is useful when the API is not hosted on the same domain as the documentation.
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}
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.
New endpoint io: form[T](name)
which:
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]
?
Re-consider the usage of optional fields
Add missing fields/classes
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:
tsec-http4s
as it is, and make another subproject for akka-http.There are two cases where the objects are not scanned:
TSchema.SCoproduct
contains a complex object - only the object itself is added - there is no recursive call to the fieldscase TSchema.SArray(o: TSchema.SCoproduct)
is not covered - it misses the objects that are defined with oneOf
if they are in TSchema.SArray
Are all of the typeclasses needed?
Do the typeclasses have correct boundaries?
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"
New endpoint io: multipart[T](name)
, which yields/accepts a Part[T]
value, which has the following parameters:
The part content can be any of the basic body types (string, byte array, file, etc.). Are streaming parts possible? Probably not.
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
Hello! Intriguing project! :)
Are there any plans or thoughts to add GraphQL-server support?
Thank you!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.