Skip to content

Commit

Permalink
Merge pull request #1174 from UdashFramework/openapi-tags
Browse files Browse the repository at this point in the history
Possibility to define OpenAPI tags description using annotations
  • Loading branch information
ddworak authored Jan 23, 2024
2 parents f9d9f06 + dc8367b commit 4e725a2
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 18 deletions.
77 changes: 76 additions & 1 deletion rest/.jvm/src/test/resources/RestTestApi.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@
},
"/failingGet": {
"get": {
"tags": [
"TrivialDescribedGroup"
],
"operationId": "failingGet",
"responses": {
"204": {
Expand Down Expand Up @@ -264,6 +267,56 @@
}
}
},
"/groupPrefix/subget/{p1}": {
"get": {
"tags": [
"GroupPrefix"
],
"operationId": "groupPrefix_subget",
"parameters": [
{
"name": "p1",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "X-H1",
"in": "header",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "q1",
"in": "query",
"required": true,
"explode": false,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/jsonFailingGet": {
"get": {
"operationId": "jsonFailingGet",
Expand Down Expand Up @@ -495,6 +548,9 @@
"/prefix/{p0}/subget/{p1}": {
"summary": "summary for prefix paths",
"get": {
"tags": [
"Prefix"
],
"operationId": "prefix_subget",
"parameters": [
{
Expand Down Expand Up @@ -642,6 +698,9 @@
},
"/trivialGet": {
"get": {
"tags": [
"TrivialGroup"
],
"operationId": "trivialGet",
"responses": {
"204": {
Expand Down Expand Up @@ -941,5 +1000,21 @@
]
}
}
}
},
"tags": [
{
"name": "Prefix",
"description": "example API subgroup"
},
{
"name": "GroupPrefix"
},
{
"name": "TrivialGroup"
},
{
"name": "TrivialDescribedGroup",
"description": "something"
}
]
}
46 changes: 34 additions & 12 deletions rest/src/main/scala/io/udash/rest/openapi/OpenApiMetadata.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ final case class OpenApiMetadata[T](
) {
val httpMethods: List[OpenApiOperation[_]] = (gets: List[OpenApiOperation[_]]) ++ customBodyMethods ++ bodyMethods

// collect all tags
private lazy val openApiTags: List[Tag] = {
def createTag(method: OpenApiMethod[_]): Opt[Tag] =
method.groupAnnot.map { group =>
method.tagAdjusters.foldLeft(Tag(group.groupName))({ case (tag, adjuster) => adjuster.adjustTag(tag) })
}

(prefixes.iterator.flatMap(prefix => createTag(prefix).iterator ++ prefix.result.value.openApiTags.iterator) ++
httpMethods.iterator.flatMap(createTag)).toList
}

def operations(resolver: SchemaResolver): Iterator[PathOperation] =
prefixes.iterator.flatMap(_.operations(resolver)) ++
httpMethods.iterator.map(_.pathOperation(resolver))
Expand Down Expand Up @@ -79,7 +90,7 @@ final case class OpenApiMetadata[T](
put = ops.getOpt(HttpMethod.PUT).toOptArg,
post = ops.getOpt(HttpMethod.POST).toOptArg,
patch = ops.getOpt(HttpMethod.PATCH).toOptArg,
delete = ops.getOpt(HttpMethod.DELETE).toOptArg
delete = ops.getOpt(HttpMethod.DELETE).toOptArg,
)
(path, RefOr(pathAdjustersMap(path).foldRight(pathItem)(_ adjustPathItem _)))
}.intoMap[ITreeMap])
Expand All @@ -94,14 +105,15 @@ final case class OpenApiMetadata[T](
externalDocs: OptArg[ExternalDocumentation] = OptArg.Empty
): OpenApi = {
val registry = new SchemaRegistry(initial = components.schemas)
OpenApi(OpenApi.Version,
OpenApi(
OpenApi.Version,
info,
paths(registry),
components = components.copy(schemas = registry.registeredSchemas),
servers = servers,
security = security,
tags = tags,
externalDocs = externalDocs
tags = openApiTags ++ tags,
externalDocs = externalDocs,
)
}
}
Expand All @@ -119,7 +131,7 @@ final case class PathOperation(
path: String,
method: HttpMethod,
operation: Operation,
pathAdjusters: List[PathItemAdjuster]
pathAdjusters: List[PathItemAdjuster],
)

sealed trait OpenApiMethod[T] extends TypedMetadata[T] {
Expand All @@ -128,6 +140,8 @@ sealed trait OpenApiMethod[T] extends TypedMetadata[T] {
@multi @rpcParamMetadata @tagged[NonBodyTag] @allowOptional def parameters: List[OpenApiParameter[_]]
@multi @reifyAnnot def operationAdjusters: List[OperationAdjuster]
@multi @reifyAnnot def pathAdjusters: List[PathItemAdjuster]
@optional @reifyAnnot def groupAnnot: Opt[group]
@multi @reifyAnnot def tagAdjusters: List[TagAdjuster]

val pathPattern: String = {
val pathParts = methodTag.path :: parameters.flatMap {
Expand All @@ -145,8 +159,10 @@ final case class OpenApiPrefix[T](
parameters: List[OpenApiParameter[_]],
operationAdjusters: List[OperationAdjuster],
pathAdjusters: List[PathItemAdjuster],
groupAnnot: Opt[group],
tagAdjusters: List[TagAdjuster],
@optional @reifyAnnot operationIdPrefix: Opt[operationIdPrefix],
@infer @checked result: OpenApiMetadata.Lazy[T]
@infer @checked result: OpenApiMetadata.Lazy[T],
) extends OpenApiMethod[T] {

def operations(resolver: SchemaResolver): Iterator[PathOperation] = {
Expand Down Expand Up @@ -188,8 +204,10 @@ final case class OpenApiGetOperation[T](
methodTag: HttpMethodTag,
operationAdjusters: List[OperationAdjuster],
pathAdjusters: List[PathItemAdjuster],
groupAnnot: Opt[group],
tagAdjusters: List[TagAdjuster],
parameters: List[OpenApiParameter[_]],
resultType: RestResultType[T]
resultType: RestResultType[T],
) extends OpenApiOperation[T] {
def requestBody(resolver: SchemaResolver): Opt[RefOr[RequestBody]] = Opt.Empty
}
Expand All @@ -199,6 +217,8 @@ final case class OpenApiCustomBodyOperation[T](
methodTag: HttpMethodTag,
operationAdjusters: List[OperationAdjuster],
pathAdjusters: List[PathItemAdjuster],
groupAnnot: Opt[group],
tagAdjusters: List[TagAdjuster],
parameters: List[OpenApiParameter[_]],
@encoded @rpcParamMetadata @tagged[Body] @unmatched(RawRest.MissingBodyParam) singleBody: OpenApiBody[_],
resultType: RestResultType[T]
Expand All @@ -212,10 +232,12 @@ final case class OpenApiBodyOperation[T](
methodTag: HttpMethodTag,
operationAdjusters: List[OperationAdjuster],
pathAdjusters: List[PathItemAdjuster],
groupAnnot: Opt[group],
tagAdjusters: List[TagAdjuster],
parameters: List[OpenApiParameter[_]],
@multi @rpcParamMetadata @tagged[Body] @allowOptional bodyFields: List[OpenApiBodyField[_]],
@reifyAnnot bodyTypeTag: BodyTypeTag,
resultType: RestResultType[T]
resultType: RestResultType[T],
) extends OpenApiOperation[T] {

def requestBody(resolver: SchemaResolver): Opt[RefOr[RequestBody]] =
Expand All @@ -238,7 +260,7 @@ final case class OpenApiParamInfo[T](
@optional @composite whenAbsentInfo: Opt[WhenAbsentInfo[T]],
@isAnnotated[optionalParam] optional: Boolean,
@reifyFlags flags: ParamFlags,
@infer restSchema: RestSchema[T]
@infer restSchema: RestSchema[T],
) extends TypedMetadata[T] {
val whenAbsentValue: Opt[JsonValue] =
if (optional) Opt.Empty
Expand All @@ -259,7 +281,7 @@ final case class OpenApiParamInfo[T](
final case class OpenApiParameter[T](
@reifyAnnot paramTag: NonBodyTag,
@composite info: OpenApiParamInfo[T],
@multi @reifyAnnot adjusters: List[ParameterAdjuster]
@multi @reifyAnnot adjusters: List[ParameterAdjuster],
) extends TypedMetadata[T] {

def parameter(resolver: SchemaResolver): RefOr[Parameter] = {
Expand All @@ -284,15 +306,15 @@ final case class OpenApiParameter[T](

final case class OpenApiBodyField[T](
@composite info: OpenApiParamInfo[T],
@multi @reifyAnnot schemaAdjusters: List[SchemaAdjuster]
@multi @reifyAnnot schemaAdjusters: List[SchemaAdjuster],
) extends TypedMetadata[T] {
def schema(resolver: SchemaResolver): RefOr[Schema] =
SchemaAdjuster.adjustRef(schemaAdjusters, info.schema(resolver, withDefaultValue = true))
}

final case class OpenApiBody[T](
@infer restRequestBody: RestRequestBody[T],
@multi @reifyAnnot schemaAdjusters: List[SchemaAdjuster]
@multi @reifyAnnot schemaAdjusters: List[SchemaAdjuster],
) extends TypedMetadata[T] {
def requestBody(resolver: SchemaResolver): Opt[RefOr[RequestBody]] = {
def transformSchema(schema: RestSchema[_]): RestSchema[_] =
Expand Down
45 changes: 42 additions & 3 deletions rest/src/main/scala/io/udash/rest/openapi/adjusters/Adjuster.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package rest.openapi
package adjusters

import com.avsystem.commons._
import com.avsystem.commons.annotation.NotInheritedFromSealedTypes
import com.avsystem.commons.annotation.{AnnotationAggregate, NotInheritedFromSealedTypes, defaultsToName}
import com.avsystem.commons.meta.infer
import com.avsystem.commons.rpc.AsRaw
import io.udash.rest.raw._
Expand Down Expand Up @@ -46,8 +46,7 @@ object SchemaAdjuster {

/**
* Base trait for annotation which may adjust [[io.udash.rest.openapi.Parameter Parameter]] generated for path,
* query or header parameters
* of REST RPC methods.
* query or header parameters of REST RPC methods.
*/
trait ParameterAdjuster extends Adjuster {
def adjustParameter(parameter: Parameter): Parameter
Expand Down Expand Up @@ -122,6 +121,15 @@ trait SuccessfulResponseAdjuster extends OperationAdjuster {
def adjustResponse(response: Response): Response
}

/**
* Base trait for annotation which may adjust [[io.udash.rest.openapi.Tag Tag]] generated for REST HTTP methods.
* Tag adjusters may also be specified on prefix methods - they will be added to all operations
* generated for the result of this prefix method.
*/
trait TagAdjuster extends StaticAnnotation {
def adjustTag(tag: Tag): Tag
}

/** Convenience implementation of [[io.udash.rest.openapi.adjusters.SchemaAdjuster SchemaAdjuster]] */
class adjustSchema(f: Schema => Schema) extends SchemaAdjuster {
def adjustSchema(value: Schema): Schema = f(value)
Expand All @@ -138,6 +146,10 @@ class adjustOperation(f: Operation => Operation) extends OperationAdjuster {
class adjustPathItem(f: PathItem => PathItem) extends PathItemAdjuster {
def adjustPathItem(value: PathItem): PathItem = f(value)
}
/** Convenience implementation of [[io.udash.rest.openapi.adjusters.TagAdjuster TagAdjuster]] */
class adjustTag(f: Tag => Tag) extends TagAdjuster {
override def adjustTag(tag: Tag): Tag = f(tag)
}

/**
* Sets the `title` of a [[io.udash.rest.openapi.Schema Schema]]. It can be applied on standard
Expand Down Expand Up @@ -253,6 +265,33 @@ class tags(tags: String*) extends OperationAdjuster {
operation.copy(tags = operation.tags ++ tags)
}

/**
* Annotation which may be applied on HTTP REST method to add a single Open API [[io.udash.rest.openapi.Tag Tag]] for
* that method. Group can be also defined on prefix methods - it will be applied to all operations generated for the
* result of this prefix method.
*
* @param name OpenAPI tag name, defaults to method name
*/
class group(@defaultsToName name: String = null) extends tags(name.capitalize) {
def groupName: String = name.capitalize
}

/**
* Annotation which may be applied on HTTP REST method to specify description for
* [[io.udash.rest.openapi.Tag Tag]] defined in that method.
*/
class tagDescription(desc: String) extends TagAdjuster {
override def adjustTag(tag: Tag): Tag = tag.copy(description = desc)
}

/**
* Shortcut aggregating both [[group]] and [[tagDescription]]. Always uses default `group` name.
*/
class describedGroup(desc: String) extends AnnotationAggregate {
@group @tagDescription(desc)
final def aggregated: List[StaticAnnotation] = reifyAggregated
}

/**
* Prefix methods may be annotated with this annotation to specify prefix that will be prepended to
* `operationId` of all [[io.udash.rest.openapi.Operation Operation]] objects generated for result of that prefix method.
Expand Down
9 changes: 7 additions & 2 deletions rest/src/test/scala/io/udash/rest/RestTestApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ case class ErrorWrapper[T](error: T)
object ErrorWrapper extends HasPolyGenCodec[ErrorWrapper]

trait RestTestApi {
@GET def trivialGet: Future[Unit]
@GET def failingGet: Future[Unit]
@GET @group("TrivialGroup") def trivialGet: Future[Unit]
@GET @group("TrivialDescribedGroup") @tagDescription("something") def failingGet: Future[Unit]
@GET def jsonFailingGet: Future[Unit]
@GET def moreFailingGet: Future[Unit]
@GET def neverGet: Future[Unit]
Expand Down Expand Up @@ -124,12 +124,16 @@ trait RestTestApi {
): Future[String]

@pathSummary("summary for prefix paths")
@describedGroup("example API subgroup")
def prefix(
p0: String,
@Header("X-H0") h0: String,
@Query @example("q0example") q0: String
): RestTestSubApi

@group
def groupPrefix: RestTestSubApi

@Prefix("") def transparentPrefix: RestTestSubApi

def complexParams(
Expand Down Expand Up @@ -171,6 +175,7 @@ object RestTestApi extends DefaultRestApiCompanion[RestTestApi] {
Future.successful(s"$q1-$p1-$p2")
def prefix(p0: String, h0: String, q0: String): RestTestSubApi =
RestTestSubApi.impl(s"$p0-$h0-$q0")
def groupPrefix: RestTestSubApi = RestTestSubApi.impl("")
def transparentPrefix: RestTestSubApi = RestTestSubApi.impl("")
def complexParams(baseEntity: BaseEntity, flatBaseEntity: Opt[FlatBaseEntity]): Future[Unit] = Future.unit
def complexParams(flatBaseEntity: FlatBaseEntity, baseEntity: Opt[BaseEntity]): Future[Unit] = Future.unit
Expand Down

0 comments on commit 4e725a2

Please sign in to comment.