Skip to content

Commit

Permalink
Add a possibility to define OpenAPI tags with descriptions using anno…
Browse files Browse the repository at this point in the history
…tations in REST framework
  • Loading branch information
sebaciv committed Jan 16, 2024
1 parent db2509e commit 05670bf
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 18 deletions.
24 changes: 23 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 @@ -495,6 +498,9 @@
"/prefix/{p0}/subget/{p1}": {
"summary": "summary for prefix paths",
"get": {
"tags": [
"Prefix"
],
"operationId": "prefix_subget",
"parameters": [
{
Expand Down Expand Up @@ -642,6 +648,9 @@
},
"/trivialGet": {
"get": {
"tags": [
"TrivialGroup"
],
"operationId": "trivialGet",
"responses": {
"204": {
Expand Down Expand Up @@ -941,5 +950,18 @@
]
}
}
}
},
"tags": [
{
"name": "Prefix",
"description": "example API subgroup"
},
{
"name": "TrivialGroup"
},
{
"name": "TrivialDescribedGroup",
"description": "something"
}
]
}
45 changes: 33 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,16 @@ final case class OpenApiMetadata[T](
) {
val httpMethods: List[OpenApiOperation[_]] = (gets: List[OpenApiOperation[_]]) ++ customBodyMethods ++ bodyMethods

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

prefixes.flatMap(prefix => createTags(prefix) ++ prefix.result.value.openApiTags) ++ httpMethods.flatMap(createTags)
}

def operations(resolver: SchemaResolver): Iterator[PathOperation] =
prefixes.iterator.flatMap(_.operations(resolver)) ++
httpMethods.iterator.map(_.pathOperation(resolver))
Expand Down Expand Up @@ -79,7 +89,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 +104,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 +130,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 +139,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 +158,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 +203,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 +216,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 +231,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 +259,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 +280,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 +305,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
5 changes: 3 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,6 +124,7 @@ trait RestTestApi {
): Future[String]

@pathSummary("summary for prefix paths")
@describedGroup("example API subgroup")
def prefix(
p0: String,
@Header("X-H0") h0: String,
Expand Down

0 comments on commit 05670bf

Please sign in to comment.