diff --git a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIDocs.scala b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIDocs.scala index f5f281abb1..9c25ed3c77 100644 --- a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIDocs.scala +++ b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIDocs.scala @@ -11,14 +11,16 @@ import scala.collection.immutable.ListMap private[openapi] object EndpointToOpenAPIDocs { def toOpenAPI( api: Info, - es: Iterable[AnyEndpoint], + es: Iterable[EndpointWithDocsMetadata], options: OpenAPIDocsOptions, docsExtensions: List[DocsExtension[_]] ): OpenAPI = { - val es2 = es.filter(e => findWebSocket(e).isEmpty).map(nameAllPathCapturesInEndpoint) + val es2 = es + .filter(e => findWebSocket(e.endpoint).isEmpty) + .map(e => e.copy(endpoint = nameAllPathCapturesInEndpoint(e.endpoint))) val toNamedSchemas = new ToNamedSchemas - val (keyToSchema, schemas) = new SchemasForEndpoints(es2, options.schemaName, toNamedSchemas).apply() - val securitySchemes = SecuritySchemesForEndpoints(es2) + val (keyToSchema, schemas) = new SchemasForEndpoints(es2.map(_.endpoint), options.schemaName, toNamedSchemas).apply() + val securitySchemes = SecuritySchemesForEndpoints(es2.map(_.endpoint)) val pathCreator = new EndpointToOpenAPIPaths(schemas, securitySchemes, options) val componentsCreator = new EndpointToOpenAPIComponents(keyToSchema, securitySchemes) diff --git a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIPaths.scala b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIPaths.scala index 92137f08a1..2706db12ac 100644 --- a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIPaths.scala +++ b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIPaths.scala @@ -15,16 +15,17 @@ private[openapi] class EndpointToOpenAPIPaths(schemas: Schemas, securitySchemes: private val codecToMediaType = new CodecToMediaType(schemas) private val endpointToOperationResponse = new EndpointToOperationResponse(schemas, codecToMediaType, options) - def pathItem(e: AnyEndpoint): (String, PathItem) = { + def pathItem(endpointWithDocsMetadata: EndpointWithDocsMetadata): (String, PathItem) = { import Method._ + val e = endpointWithDocsMetadata.endpoint val inputs = e.asVectorOfBasicInputs(includeAuth = false) val pathComponents = namedPathComponents(inputs) val method = e.method.getOrElse(Method.GET) val defaultId = options.operationIdGenerator(e, pathComponents, method) - val operation = Some(endpointToOperation(defaultId, e, inputs)) + val operation = Some(endpointToOperation(defaultId, endpointWithDocsMetadata, inputs)) val pathItem = PathItem( get = if (method == GET) operation else None, put = if (method == PUT) operation else None, @@ -39,7 +40,12 @@ private[openapi] class EndpointToOpenAPIPaths(schemas: Schemas, securitySchemes: (e.renderPathTemplate(renderQueryParam = None, includeAuth = false), pathItem) } - private def endpointToOperation(defaultId: String, e: AnyEndpoint, inputs: Vector[EndpointInput.Basic[_]]): Operation = { + private def endpointToOperation( + defaultId: String, + endpointWithDocsMetadata: EndpointWithDocsMetadata, + inputs: Vector[EndpointInput.Basic[_]] + ): Operation = { + val e = endpointWithDocsMetadata.endpoint val parameters = operationParameters(inputs) val body: Vector[ReferenceOr[RequestBody]] = operationInputBody(inputs) val responses: ListMap[ResponsesKey, ReferenceOr[Response]] = endpointToOperationResponse(e) @@ -54,6 +60,7 @@ private[openapi] class EndpointToOpenAPIPaths(schemas: Schemas, securitySchemes: Responses(responses), if (e.info.deprecated) Some(true) else None, operationSecurity(e), + servers = endpointWithDocsMetadata.servers, extensions = DocsExtensions.fromIterable(e.info.docsExtensions) ) } diff --git a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointWithDocsMetadata.scala b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointWithDocsMetadata.scala new file mode 100644 index 0000000000..1b81d7a619 --- /dev/null +++ b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointWithDocsMetadata.scala @@ -0,0 +1,8 @@ +package sttp.tapir.docs.openapi +import sttp.tapir.AnyEndpoint +import sttp.tapir.openapi.Server + +case class EndpointWithDocsMetadata( + endpoint: AnyEndpoint, + servers: List[Server] = Nil +) diff --git a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/OpenAPIDocsInterpreter.scala b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/OpenAPIDocsInterpreter.scala index 7e18082dd6..98fb1000cf 100644 --- a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/OpenAPIDocsInterpreter.scala +++ b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/OpenAPIDocsInterpreter.scala @@ -12,45 +12,71 @@ trait OpenAPIDocsInterpreter { toOpenAPI(e, Info(title, version)) def toOpenAPI[A, I, E, O, R](e: Endpoint[A, I, E, O, R], info: Info): OpenAPI = - EndpointToOpenAPIDocs.toOpenAPI(info, Seq(e), openAPIDocsOptions, List.empty) + EndpointToOpenAPIDocs.toOpenAPI(info, Seq(EndpointWithDocsMetadata(e)), openAPIDocsOptions, List.empty) def toOpenAPI[A, I, E, O, R](e: Endpoint[A, I, E, O, R], info: Info, docsExtensions: List[DocsExtension[_]]): OpenAPI = - EndpointToOpenAPIDocs.toOpenAPI(info, Seq(e), openAPIDocsOptions, docsExtensions) + EndpointToOpenAPIDocs.toOpenAPI(info, Seq(EndpointWithDocsMetadata(e)), openAPIDocsOptions, docsExtensions) def toOpenAPI[R, F[_]](se: ServerEndpoint[R, F], title: String, version: String): OpenAPI = toOpenAPI(se.endpoint, Info(title, version)) def toOpenAPI[R, F[_]](se: ServerEndpoint[R, F], info: Info): OpenAPI = - EndpointToOpenAPIDocs.toOpenAPI(info, Seq(se.endpoint), openAPIDocsOptions, List.empty) + EndpointToOpenAPIDocs.toOpenAPI(info, Seq(EndpointWithDocsMetadata(se.endpoint)), openAPIDocsOptions, List.empty) def toOpenAPI[R, F[_]]( se: ServerEndpoint[R, F], info: Info, docsExtensions: List[DocsExtension[_]] ): OpenAPI = - EndpointToOpenAPIDocs.toOpenAPI(info, Seq(se.endpoint), openAPIDocsOptions, docsExtensions) + EndpointToOpenAPIDocs.toOpenAPI(info, Seq(EndpointWithDocsMetadata(se.endpoint)), openAPIDocsOptions, docsExtensions) + + def toOpenAPI(e: EndpointWithDocsMetadata, title: String, version: String): OpenAPI = + toOpenAPI(e, Info(title, version)) + + def toOpenAPI(e: EndpointWithDocsMetadata, info: Info): OpenAPI = + EndpointToOpenAPIDocs.toOpenAPI(info, Seq(e), openAPIDocsOptions, List.empty) + + def toOpenAPI( + e: EndpointWithDocsMetadata, + info: Info, + docsExtensions: List[DocsExtension[_]] + ): OpenAPI = + EndpointToOpenAPIDocs.toOpenAPI(info, Seq(e), openAPIDocsOptions, docsExtensions) def toOpenAPI(es: Iterable[AnyEndpoint], title: String, version: String): OpenAPI = toOpenAPI(es, Info(title, version)) def toOpenAPI(es: Iterable[AnyEndpoint], info: Info): OpenAPI = - EndpointToOpenAPIDocs.toOpenAPI(info, es, openAPIDocsOptions, List.empty) + EndpointToOpenAPIDocs.toOpenAPI(info, es.map(EndpointWithDocsMetadata(_)), openAPIDocsOptions, List.empty) def toOpenAPI(es: Iterable[AnyEndpoint], info: Info, docsExtensions: List[DocsExtension[_]]): OpenAPI = - EndpointToOpenAPIDocs.toOpenAPI(info, es, openAPIDocsOptions, docsExtensions) + EndpointToOpenAPIDocs.toOpenAPI(info, es.map(EndpointWithDocsMetadata(_)), openAPIDocsOptions, docsExtensions) def serverEndpointsToOpenAPI[F[_]](ses: Iterable[ServerEndpoint[_, F]], title: String, version: String): OpenAPI = serverEndpointsToOpenAPI(ses, Info(title, version)) def serverEndpointsToOpenAPI[F[_]](ses: Iterable[ServerEndpoint[_, F]], info: Info): OpenAPI = - EndpointToOpenAPIDocs.toOpenAPI(info, ses.map(_.endpoint), openAPIDocsOptions, List.empty) + EndpointToOpenAPIDocs.toOpenAPI(info, ses.map(e => EndpointWithDocsMetadata(e.endpoint)), openAPIDocsOptions, List.empty) def serverEndpointsToOpenAPI[F[_]]( ses: Iterable[ServerEndpoint[_, F]], info: Info, docsExtensions: List[DocsExtension[_]] ): OpenAPI = - EndpointToOpenAPIDocs.toOpenAPI(info, ses.map(_.endpoint), openAPIDocsOptions, docsExtensions) + EndpointToOpenAPIDocs.toOpenAPI(info, ses.map(e => EndpointWithDocsMetadata(e.endpoint)), openAPIDocsOptions, docsExtensions) + + def endpointsWithDocsMetadataToOpenAPI(es: Iterable[EndpointWithDocsMetadata], title: String, version: String): OpenAPI = + endpointsWithDocsMetadataToOpenAPI(es, Info(title, version)) + + def endpointsWithDocsMetadataToOpenAPI(es: Iterable[EndpointWithDocsMetadata], info: Info): OpenAPI = + EndpointToOpenAPIDocs.toOpenAPI(info, es, openAPIDocsOptions, List.empty) + + def endpointsWithDocsMetadataToOpenAPI( + es: Iterable[EndpointWithDocsMetadata], + info: Info, + docsExtensions: List[DocsExtension[_]] + ): OpenAPI = + EndpointToOpenAPIDocs.toOpenAPI(info, es, openAPIDocsOptions, docsExtensions) } object OpenAPIDocsInterpreter { diff --git a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/package.scala b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/package.scala new file mode 100644 index 0000000000..639b4f9122 --- /dev/null +++ b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/package.scala @@ -0,0 +1,11 @@ +package sttp.tapir.docs +import sttp.tapir.AnyEndpoint +import sttp.tapir.openapi.Server + +package object openapi { + + implicit class EndpointWithDocsMetadataImplicits(endpoint: AnyEndpoint) { + def withServers(servers: List[Server]): EndpointWithDocsMetadata = EndpointWithDocsMetadata(endpoint, servers) + def withoutServers(): EndpointWithDocsMetadata = EndpointWithDocsMetadata(endpoint) + } +} diff --git a/docs/swagger-ui-bundle/src/main/scala/sttp/tapir/swagger/bundle/SwaggerInterpreter.scala b/docs/swagger-ui-bundle/src/main/scala/sttp/tapir/swagger/bundle/SwaggerInterpreter.scala index 01ff2b8dba..5c83f9f2ea 100644 --- a/docs/swagger-ui-bundle/src/main/scala/sttp/tapir/swagger/bundle/SwaggerInterpreter.scala +++ b/docs/swagger-ui-bundle/src/main/scala/sttp/tapir/swagger/bundle/SwaggerInterpreter.scala @@ -1,7 +1,7 @@ package sttp.tapir.swagger.bundle import sttp.tapir.{AnyEndpoint, DocsExtension} -import sttp.tapir.docs.openapi.{OpenAPIDocsInterpreter, OpenAPIDocsOptions} +import sttp.tapir.docs.openapi.{EndpointWithDocsMetadata, OpenAPIDocsInterpreter, OpenAPIDocsOptions} import sttp.tapir.openapi.Info import sttp.tapir.openapi.circe.yaml._ import sttp.tapir.server.ServerEndpoint @@ -19,7 +19,7 @@ trait SwaggerInterpreter { info: Info ): List[ServerEndpoint[Any, F]] = { val yaml = OpenAPIDocsInterpreter(docsOptions).toOpenAPI(endpoints, info).toYaml - SwaggerUI(yaml, prefix, yamlName, basePrefix) + swaggerUI[F](yaml) } def fromEndpoints[F[_]]( @@ -40,6 +40,22 @@ trait SwaggerInterpreter { version: String ): List[ServerEndpoint[Any, F]] = fromEndpoints(endpoints.map(_.endpoint), Info(title, version)) + + def fromEndpointsWithDocsMetadata[F[_]]( + endpoints: List[EndpointWithDocsMetadata], + info: Info + ): List[ServerEndpoint[Any, F]] = { + val yaml = OpenAPIDocsInterpreter(docsOptions).endpointsWithDocsMetadataToOpenAPI(endpoints, info).toYaml + swaggerUI[F](yaml) + } + + def fromEndpointsWithDocsMetadata[F[_]]( + endpoints: List[EndpointWithDocsMetadata], + title: String, + version: String + ): List[ServerEndpoint[Any, F]] = fromEndpointsWithDocsMetadata(endpoints, Info(title, version)) + + private def swaggerUI[F[_]](yaml: String): List[ServerEndpoint[Any, F]] = SwaggerUI(yaml, prefix, yamlName, basePrefix) } object SwaggerInterpreter {