tkaczmarzyk / specification-arg-resolver

An alternative API for filtering data with Spring MVC & Spring Data JPA
Apache License 2.0
653 stars 149 forks source link

Document the specification interface using swagger #60

Closed vegegoku closed 1 year ago

vegegoku commented 4 years ago

This is more of a question than an issue, i would love to have the specifications interface to showup in my swagger documentation, the interface is supposed to be resolved as a set of query parameters, i tried to add some swagger annotations on the interface but this didnt make it visible in the swagger docs, how would you go around this?

tinhpt94 commented 4 years ago

This is more of a question than an issue, i would love to have the specifications interface to showup in my swagger documentation, the interface is supposed to be resolved as a set of query parameters, i tried to add some swagger annotations on the interface but this didnt make it visible in the swagger docs, how would you go around this?

Hi vegegoku, I start using this library and face same issue with you. I found a work around solution as below:

    @ApiOperation("Return list of customers")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "teleSaleId", value = "Tele Sale Id", dataType = "long", example = "1", paramType = "query"),
            @ApiImplicitParam(name = "saleId", value = "Sale Id", dataType = "long", example = "2", paramType = "query"),
            @ApiImplicitParam(name = "saleAdminId", value = "Sale Admin Id", dataType = "long", example = "3", paramType = "query"),
            @ApiImplicitParam(name = "saleManagerId", value = "Sale Manager Id", dataType = "long", example = "4", paramType = "query")
    })
    @GetMapping
    public ResponseEntity<List<CustomerResponse>> getAllCustomer(CustomerSpec customerSpec) {
        return ResponseEntity.ok(customerService.getAllCustomer(customerSpec));
    }
@And({
        @Spec(path = "teleSaleId", params = "teleSaleId", spec = Equal.class),
        @Spec(path = "saleId", params = "saleId", spec = Equal.class),
        @Spec(path = "saleAdminId", params = "saleAdminId", spec = Equal.class),
        @Spec(path = "saleManagerId", params = "saleManagerId", spec = Equal.class),
})
public interface CustomerSpec extends Specification<CustomerEntity> {
}
vegegoku commented 4 years ago

@tinhpt94 this exactly like my solution, and those 2 interfaces are both auto-generated for me using an annotation processor.

mdekhtiarenko commented 4 years ago

Swagger has OperationBuilderPlugin interface. By implementing this interface you can teach swagger about annotations that are unknown to it. We created a component that implements it where we looked for all @Spec and @Andannotations and add them to swagger definition. That could be a good alternative to using @ApiImplicitParam because you write it just once and it works for all endpoints in your project.

I was wondering if that's a good idea to make it part of the library to support swagger automatically. What do you think about this @tkaczmarzyk?

s-frei commented 3 years ago

Would be awesome to modify (copy) that configuration to make it also work for springdoc, when necessary.

randyhbh commented 3 years ago

If I understood your question right you want that the parameters that you use for the Specification show up in the Swagger docs. I solved this in a completely different way, without any extra code or the need to implement something new.

BTW: this is Kotlin code but works in the same way for Java.

    @ApiOperation("Returns all authorization")
    @GetMapping("/authorization")
    fun getAuthorizationListBy(
        @RequestParam(required = false) referenceId: Long?,
        @RequestParam(required = false) useCase: String?,
        @RequestParam(required = false) status: String?,
        @RequestParam(required = false) paymentMethodId: Long?,
        @RequestParam(required = false) paymentMethodType: String?,
        authorizationSpecification: AuthorizationSpecification
    ): List<AuthorizationTO> =
            authorizationProvider.findAuthorizationBy(authorizationSpecification)

And my specification interface is

@And(
    Spec(path = "status", spec = Equal::class),
    Spec(path = "useCase", spec = Equal::class),
    Spec(path = "referenceId", spec = Equal::class),
    Spec(path = "paymentMethod", spec = Equal::class),
    Spec(path = "paymentMethodId", spec = Equal::class)
)
interface AuthorizationSpecification : Specification<Authorization>
LucasPenido commented 2 years ago

Hello @mdekhtiarenko I am interested in this interface to make swagger learn the annotations of this library. I would like to know if you have implemented this or have an example that could help.

3LexW commented 2 years ago

First, big thanks to @mdekhtiarenko on providing the idea.

I am using the OperationCustomizer from springdoc-openapi, that find if the Specification class exists in method parameter. I am using only @Join, @And and @Spec annotations for now, you can perform your own customization if needed. The code is quite ugly and I am looking for help on refactor.

The code is in Kotlin by the way, which is easy to convert them back to Java.

build.gradle.kts

implementation("org.springdoc:springdoc-openapi-ui:1.6.9")
implementation("org.springdoc:springdoc-openapi-kotlin:1.6.9")
implementation("org.springdoc:springdoc-openapi-security:1.6.9")

com.example.api.docs.SpecificationArgOperationCustomizer.kt

package com.example.api.docs

import io.swagger.v3.oas.models.Operation
import io.swagger.v3.oas.models.media.Schema
import io.swagger.v3.oas.models.parameters.Parameter
import net.kaczmarzyk.spring.data.jpa.web.annotation.And
import net.kaczmarzyk.spring.data.jpa.web.annotation.Join
import net.kaczmarzyk.spring.data.jpa.web.annotation.RepeatedJoin
import net.kaczmarzyk.spring.data.jpa.web.annotation.Spec
import org.springdoc.core.customizers.OperationCustomizer
import org.springframework.data.jpa.domain.Specification
import org.springframework.stereotype.Component
import org.springframework.web.method.HandlerMethod

@Component
/**
 * Catch the Specification Arg and add the specs into the springdoc
 */
class SpecificationArgOperationCustomizer : OperationCustomizer {
    override fun customize(operation: Operation?, handlerMethod: HandlerMethod?): Operation? {
        if (handlerMethod != null && operation != null) {
            for (methodParameter in handlerMethod.methodParameters) {
                for (cls in methodParameter.parameterType.interfaces) {
                    if (cls.name == Specification::class.qualifiedName) {

                        // Obtain the list of joins of the specification
                        val joinMap = HashMap<String, String>()
                        for (anno in methodParameter.parameterType.annotations) {
                            if (anno.annotationClass.qualifiedName == Join::class.qualifiedName) {
                                joinMap[(anno as Join).alias] = (anno as Join).path
                            }
                            if (anno.annotationClass.qualifiedName == RepeatedJoin::class.qualifiedName){
                                for (join in (anno as RepeatedJoin).value){
                                    joinMap[join.alias] = join.path
                                }
                            }
                        }

                        // Create doc param from annotations
                        for (anno in methodParameter.parameterType.annotations) {
                            if (anno.annotationClass.qualifiedName == Spec::class.qualifiedName){
                                createParameter(anno as Spec, joinMap).map {
                                    operation.addParametersItem(it)
                                }
                            }
                            if (anno.annotationClass.qualifiedName == And::class.qualifiedName) {
                                for (spec in (anno as And).value) {
                                    createParameter(spec, joinMap).map {
                                        operation.addParametersItem(it)
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return operation
    }

    fun createParameter(spec: Spec, joinMap: Map<String, String>): MutableList<Parameter> {
        val result = mutableListOf<Parameter>()

        val paramSchema = Schema<String>()
        paramSchema.type = "string"
        paramSchema.setDefault(null)

        for (paramString in spec.params) {

            // Get the alias if any join exists, perform while loop for nested loops
            var path = spec.path
            while (path.contains(".")) {
                val splitStr = path.split(".")
                path = joinMap[splitStr[0]] + ":" + splitStr[1]
            }

            val newParam = Parameter()
            newParam.name = paramString
            newParam.description =
                "Will search for parameter $path using matching method: ${spec.spec.simpleName}"
            newParam.required = false
            newParam.`in` = "query"
            newParam.schema = paramSchema

            result.add(newParam)

        }
        return result
    }

}

Finally, you can hide the specification parameter by adding annotation @Parameter(hidden = true) to it, so that the annotation will not be shown in the UI and the json files

@Parameter(hidden = true) exampleSpecification: ExampleSpecification?,

The final result should be something like this image

tkaczmarzyk commented 1 year ago

included in https://github.com/tkaczmarzyk/specification-arg-resolver/pull/142, will be released today in v2.12.0