specification-arg-resolver icon indicating copy to clipboard operation
specification-arg-resolver copied to clipboard

Document the specification interface using swagger

Open vegegoku opened this issue 5 years ago • 7 comments

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?

vegegoku avatar Oct 06 '19 07:10 vegegoku

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> {
}

tinhpt94 avatar Nov 27 '19 10:11 tinhpt94

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

vegegoku avatar Nov 27 '19 12:11 vegegoku

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 @And annotations 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?

mdekhtiarenko avatar May 30 '20 16:05 mdekhtiarenko

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

s-frei avatar Nov 12 '20 10:11 s-frei

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>

randyhbh avatar Feb 08 '21 11:02 randyhbh

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.

LucasPenido avatar Oct 28 '21 13:10 LucasPenido

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

3LexW avatar Aug 25 '22 07:08 3LexW

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

tkaczmarzyk avatar Dec 06 '22 13:12 tkaczmarzyk