diff --git a/docs/openapi-docs/src/main/scala/tapir/docs/openapi/EndpointToOpenAPIDocs.scala b/docs/openapi-docs/src/main/scala/tapir/docs/openapi/EndpointToOpenAPIDocs.scala index a670dfdcee..1b75fc4e9c 100644 --- a/docs/openapi-docs/src/main/scala/tapir/docs/openapi/EndpointToOpenAPIDocs.scala +++ b/docs/openapi-docs/src/main/scala/tapir/docs/openapi/EndpointToOpenAPIDocs.scala @@ -5,18 +5,13 @@ import tapir.openapi._ import tapir.{EndpointInput, _} object EndpointToOpenAPIDocs { - def toOpenAPI(title: String, version: String, es: Iterable[Endpoint[_, _, _, _]], options: OpenAPIDocsOptions): OpenAPI = { + def toOpenAPI(api: Info, es: Iterable[Endpoint[_, _, _, _]], options: OpenAPIDocsOptions): OpenAPI = { val es2 = es.map(nameAllPathCapturesInEndpoint) val objectSchemas = ObjectSchemasForEndpoints(es2) val pathCreator = new EndpointToOpenApiPaths(objectSchemas, options) val componentsCreator = new EndpointToOpenApiComponents(objectSchemas) - val base = OpenAPI( - info = Info(title, None, None, version), - servers = List.empty, - paths = Map.empty, - components = componentsCreator.components - ) + val base = apiToOpenApi(api, componentsCreator) es2.map(pathCreator.pathItem).foldLeft(base) { case (current, (path, pathItem)) => @@ -24,6 +19,15 @@ object EndpointToOpenAPIDocs { } } + private def apiToOpenApi(info: Info, componentsCreator: EndpointToOpenApiComponents): OpenAPI = { + OpenAPI( + info = info, + servers = List.empty, + paths = Map.empty, + components = componentsCreator.components + ) + } + private def nameAllPathCapturesInEndpoint(e: Endpoint[_, _, _, _]): Endpoint[_, _, _, _] = { val (input2, _) = new EndpointInputMapper[Int]( { diff --git a/docs/openapi-docs/src/main/scala/tapir/docs/openapi/OpenAPIDocs.scala b/docs/openapi-docs/src/main/scala/tapir/docs/openapi/OpenAPIDocs.scala index 495408b068..9476e53c5f 100644 --- a/docs/openapi-docs/src/main/scala/tapir/docs/openapi/OpenAPIDocs.scala +++ b/docs/openapi-docs/src/main/scala/tapir/docs/openapi/OpenAPIDocs.scala @@ -1,15 +1,15 @@ package tapir.docs.openapi import tapir.Endpoint -import tapir.openapi.OpenAPI +import tapir.openapi.{Info, OpenAPI} trait OpenAPIDocs { implicit class RichOpenAPIEndpoint[I, E, O, S](e: Endpoint[I, E, O, S]) { - def toOpenAPI(title: String, version: String)(implicit options: OpenAPIDocsOptions): OpenAPI = - EndpointToOpenAPIDocs.toOpenAPI(title, version, Seq(e), options) + def toOpenAPI(info: Info)(implicit options: OpenAPIDocsOptions): OpenAPI = + EndpointToOpenAPIDocs.toOpenAPI(info, Seq(e), options) } implicit class RichOpenAPIEndpoints(es: Iterable[Endpoint[_, _, _, _]]) { - def toOpenAPI(title: String, version: String)(implicit options: OpenAPIDocsOptions): OpenAPI = - EndpointToOpenAPIDocs.toOpenAPI(title, version, es, options) + def toOpenAPI(info: Info)(implicit options: OpenAPIDocsOptions): OpenAPI = + EndpointToOpenAPIDocs.toOpenAPI(info, es, options) } } diff --git a/docs/openapi-docs/src/test/resources/expected_general_info.yml b/docs/openapi-docs/src/test/resources/expected_general_info.yml new file mode 100644 index 0000000000..c494da2e43 --- /dev/null +++ b/docs/openapi-docs/src/test/resources/expected_general_info.yml @@ -0,0 +1,35 @@ +openapi: 3.0.1 +info: + title: Fruits + version: '1.0' + description: Fruits are awesome + termsOfService: our.terms.of.service + contact: + name: Author + email: tapir@softwaremill.com + url: tapir.io + license: + name: MIT + url: mit.license +paths: + /: + get: + operationId: root-get + parameters: + - name: fruit + in: query + required: true + schema: + type: string + - name: amount + in: query + required: false + schema: + type: integer + responses: + '200': + description: '' + content: + text/plain: + schema: + type: string \ No newline at end of file diff --git a/docs/openapi-docs/src/test/scala/tapir/docs/openapi/EndpointToOpenAPIDocsTest.scala b/docs/openapi-docs/src/test/scala/tapir/docs/openapi/EndpointToOpenAPIDocsTest.scala index f97238f8eb..fa65d354ba 100644 --- a/docs/openapi-docs/src/test/scala/tapir/docs/openapi/EndpointToOpenAPIDocsTest.scala +++ b/docs/openapi-docs/src/test/scala/tapir/docs/openapi/EndpointToOpenAPIDocsTest.scala @@ -1,12 +1,13 @@ package tapir.docs.openapi import org.scalatest.{FunSuite, Matchers} +import tapir.openapi.Info import tapir.tests._ class EndpointToOpenAPIDocsTest extends FunSuite with Matchers { for (e <- allTestEndpoints) { test(s"${e.show} should convert to open api") { - e.toOpenAPI("title", "19.2-beta-RC1") + e.toOpenAPI(Info("title", "19.2-beta-RC1")) } } diff --git a/docs/openapi-docs/src/test/scala/tapir/docs/openapi/VerifyYamlTest.scala b/docs/openapi-docs/src/test/scala/tapir/docs/openapi/VerifyYamlTest.scala index 6f22df2e2c..aa74425ec5 100644 --- a/docs/openapi-docs/src/test/scala/tapir/docs/openapi/VerifyYamlTest.scala +++ b/docs/openapi-docs/src/test/scala/tapir/docs/openapi/VerifyYamlTest.scala @@ -5,6 +5,7 @@ import org.scalatest.{FunSuite, Matchers} import tapir._ import tapir.json.circe._ import tapir.openapi.circe.yaml._ +import tapir.openapi.{Contact, Info, License} import tapir.tests._ import scala.io.Source @@ -20,7 +21,7 @@ class VerifyYamlTest extends FunSuite with Matchers { test("should match the expected yaml") { val expectedYaml = loadYaml("expected.yml") - val actualYaml = List(in_query_query_out_string, all_the_way).toOpenAPI("Fruits", "1.0").toYaml + val actualYaml = List(in_query_query_out_string, all_the_way).toOpenAPI(Info("Fruits", "1.0")).toYaml val actualYamlNoIndent = noIndentation(actualYaml) actualYamlNoIndent shouldBe expectedYaml @@ -32,7 +33,7 @@ class VerifyYamlTest extends FunSuite with Matchers { test("should match the expected yaml when schema is recursive") { val expectedYaml = loadYaml("expected_recursive.yml") - val actualYaml = endpoint_wit_recursive_structure.toOpenAPI("Fruits", "1.0").toYaml + val actualYaml = endpoint_wit_recursive_structure.toOpenAPI(Info("Fruits", "1.0")).toYaml val actualYamlNoIndent = noIndentation(actualYaml) actualYamlNoIndent shouldBe expectedYaml @@ -46,7 +47,7 @@ class VerifyYamlTest extends FunSuite with Matchers { val actualYaml = in_query_query_out_string .in("add") .in("path") - .toOpenAPI("Fruits", "1.0")(options) + .toOpenAPI(Info("Fruits", "1.0"))(options) .toYaml noIndentation(actualYaml) shouldBe expectedYaml } @@ -58,7 +59,7 @@ class VerifyYamlTest extends FunSuite with Matchers { test("should match the expected yaml for streaming endpoints") { val expectedYaml = loadYaml("expected_streaming.yml") - val actualYaml = streaming_endpoint.toOpenAPI("Fruits", "1.0").toYaml + val actualYaml = streaming_endpoint.toOpenAPI(Info("Fruits", "1.0")).toYaml val actualYamlNoIndent = noIndentation(actualYaml) actualYamlNoIndent shouldBe expectedYaml @@ -71,7 +72,25 @@ class VerifyYamlTest extends FunSuite with Matchers { val expectedYaml = loadYaml("expected_tags.yml") - val actualYaml = List(userTaggedEndpointShow, userTaggedEdnpointSearch, adminTaggedEndpointAdd).toOpenAPI("Fruits", "1.0").toYaml + val actualYaml = List(userTaggedEndpointShow, userTaggedEdnpointSearch, adminTaggedEndpointAdd).toOpenAPI(Info("Fruits", "1.0")).toYaml + val actualYamlNoIndent = noIndentation(actualYaml) + + actualYamlNoIndent shouldBe expectedYaml + } + + test("should match the expected yaml for general info") { + val expectedYaml = loadYaml("expected_general_info.yml") + + val api = Info( + "Fruits", + "1.0", + description = Some("Fruits are awesome"), + termsOfService = Some("our.terms.of.service"), + contact = Some(Contact(Some("Author"), Some("tapir@softwaremill.com"), Some("tapir.io"))), + license = Some(License("MIT", Some("mit.license"))) + ) + + val actualYaml = in_query_query_out_string.toOpenAPI(api).toYaml val actualYamlNoIndent = noIndentation(actualYaml) actualYamlNoIndent shouldBe expectedYaml diff --git a/openapi/openapi-circe/src/main/scala/tapir/openapi/circe/package.scala b/openapi/openapi-circe/src/main/scala/tapir/openapi/circe/package.scala index a4f4db76a1..b2ad6b4e51 100644 --- a/openapi/openapi-circe/src/main/scala/tapir/openapi/circe/package.scala +++ b/openapi/openapi-circe/src/main/scala/tapir/openapi/circe/package.scala @@ -46,6 +46,8 @@ trait Encoders { implicit val encoderComponents: Encoder[Components] = deriveMagnoliaEncoder[Components] implicit val encoderServer: Encoder[Server] = deriveMagnoliaEncoder[Server] implicit val encoderInfo: Encoder[Info] = deriveMagnoliaEncoder[Info] + implicit val encoderContact: Encoder[Contact] = deriveMagnoliaEncoder[Contact] + implicit val encoderLicense: Encoder[License] = deriveMagnoliaEncoder[License] implicit val encoderOpenAPI: Encoder[OpenAPI] = deriveMagnoliaEncoder[OpenAPI] implicit def encodeList[T: Encoder]: Encoder[List[T]] = { case Nil => Json.Null diff --git a/openapi/openapi-model/src/main/scala/tapir/openapi/OpenAPI.scala b/openapi/openapi-model/src/main/scala/tapir/openapi/OpenAPI.scala index 8b7c1aac91..f2be2eac49 100644 --- a/openapi/openapi-model/src/main/scala/tapir/openapi/OpenAPI.scala +++ b/openapi/openapi-model/src/main/scala/tapir/openapi/OpenAPI.scala @@ -1,6 +1,6 @@ package tapir.openapi -import OpenAPI.ReferenceOr +import tapir.openapi.OpenAPI.ReferenceOr // todo security, tags, externaldocs case class OpenAPI(openapi: String = "3.0.1", @@ -23,14 +23,18 @@ object OpenAPI { type ReferenceOr[T] = Either[Reference, T] } -// todo: contact, license case class Info( title: String, - description: Option[String], - termsOfService: Option[String], - version: String + version: String, + description: Option[String] = None, + termsOfService: Option[String] = None, + contact: Option[Contact] = None, + license: Option[License] = None ) +case class Contact(name: Option[String], email: Option[String], url: Option[String]) +case class License(name: String, url: Option[String]) + // todo: variables case class Server( url: String, diff --git a/playground/src/main/scala/tapir/example/BooksExample.scala b/playground/src/main/scala/tapir/example/BooksExample.scala index c86d8e4d88..cd9f31313e 100644 --- a/playground/src/main/scala/tapir/example/BooksExample.scala +++ b/playground/src/main/scala/tapir/example/BooksExample.scala @@ -2,6 +2,7 @@ package tapir.example import com.typesafe.scalalogging.StrictLogging import tapir.example.Endpoints.Limit +import tapir.openapi.Info case class Book(title: String, genre: String, year: Int) case class BooksQuery(genre: Option[String], limit: Limit) @@ -49,7 +50,8 @@ object BooksExample extends App with StrictLogging { import tapir.openapi.circe.yaml._ // interpreting the endpoint description to generate yaml openapi documentation - val docs = List(addBook, booksListing, booksListingByGenre).toOpenAPI("The Tapir Library", "1.0") + val api = Info("The Tapir Library", "1.0") + val docs = List(addBook, booksListing, booksListingByGenre).toOpenAPI(api) docs.toYaml } diff --git a/playground/src/main/scala/tapir/tests/Tests.scala b/playground/src/main/scala/tapir/tests/Tests.scala index f75b3c9fe5..678f256dc0 100644 --- a/playground/src/main/scala/tapir/tests/Tests.scala +++ b/playground/src/main/scala/tapir/tests/Tests.scala @@ -11,6 +11,7 @@ import tapir._ import tapir.server.akkahttp._ import tapir.client.sttp._ import tapir.docs.openapi._ +import tapir.openapi.Info import scala.concurrent.{Await, Future} import scala.concurrent.duration._ @@ -50,7 +51,7 @@ object Tests extends App { import tapir.openapi.circe.yaml._ - val docs = e.toOpenAPI("Example 1", "1.0") + val docs = e.toOpenAPI(Info("Example 1", "1.0")) println("XXX") println(docs.toYaml) println("YYY") diff --git a/playground/src/main/scala/tapir/tests/Tests2.scala b/playground/src/main/scala/tapir/tests/Tests2.scala index 1bc9db779d..6586ba0658 100644 --- a/playground/src/main/scala/tapir/tests/Tests2.scala +++ b/playground/src/main/scala/tapir/tests/Tests2.scala @@ -5,6 +5,7 @@ import tapir.docs.openapi._ import tapir.openapi.circe.yaml._ import io.circe.generic.auto._ import tapir.json.circe._ +import tapir.openapi.Info object Tests2 extends App { case class Address(street: String, number: Option[Int]) @@ -16,7 +17,7 @@ object Tests2 extends App { .in(query[Option[String]]("q3")) .out(jsonBody[User].example(User("x", 10, Address("y", Some(20))))) - val docs = e.toOpenAPI("Example 1", "1.0") + val docs = e.toOpenAPI(Info("Example 1", "1.0")) println(docs.toYaml) } diff --git a/playground/src/main/scala/tapir/tests/Tests4.scala b/playground/src/main/scala/tapir/tests/Tests4.scala index 2c8845db53..5498b91ca9 100644 --- a/playground/src/main/scala/tapir/tests/Tests4.scala +++ b/playground/src/main/scala/tapir/tests/Tests4.scala @@ -1,4 +1,5 @@ package tapir.tests +import tapir.openapi.Info object Tests4 { import tapir._ @@ -24,7 +25,7 @@ object Tests4 { import tapir.openapi.circe._ import tapir.openapi.circe.yaml._ - val docs = booksListing.toOpenAPI("My Bookshop", "1.0") + val docs = booksListing.toOpenAPI(Info("My Bookshop", "1.0")) println(docs.toYaml) }