Skip to content

Commit

Permalink
Feature: Add dynamic query parameters to client calls (#311)
Browse files Browse the repository at this point in the history
* Additional inclusion of query parameters

* Complete testing
  • Loading branch information
hyperschwartz authored Sep 5, 2024
1 parent 97ff3c6 commit 10c5a67
Show file tree
Hide file tree
Showing 21 changed files with 297 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.cjbooms.fabrikt.clients

import com.example.client.ExamplePath1Client
import com.example.models.FirstModel
import com.example.models.QueryResult
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.common.ConsoleNotifier
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.options
import com.marcinziolo.kotlin.wiremock.contains
import com.marcinziolo.kotlin.wiremock.get
import com.marcinziolo.kotlin.wiremock.like
import com.marcinziolo.kotlin.wiremock.returns
import java.net.ServerSocket
import okhttp3.OkHttpClient
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AdditionalQueryParametersTest {
private val port: Int = ServerSocket(0).use { socket -> socket.localPort }
private val wiremock: WireMockServer = WireMockServer(options().port(port).notifier(ConsoleNotifier(true)))
private val mapper = ObjectMapper()
private val httpClient = OkHttpClient.Builder().build()
private val examplePath1Client = ExamplePath1Client(mapper, "http://localhost:$port", httpClient)

@BeforeEach
fun setUp() {
wiremock.start()
}

@AfterEach
fun afterEach() {
wiremock.resetAll()
wiremock.stop()
}

@Test
fun `additional query parameters are properly appended to requests`() {
val expectedResponse = QueryResult(listOf(FirstModel(id = "the parameter was there!")))
wiremock.get {
urlPath like "/example-path-1"
queryParams contains "unspecified_param" like "some_value"
} returns {
statusCode = 200
body = mapper.writeValueAsString(expectedResponse)
}
val result = examplePath1Client.getExamplePath1(additionalQueryParameters = mapOf("unspecified_param" to "some_value"))
assertThat(result.data).isEqualTo(expectedResponse)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -266,4 +266,4 @@ class Okio3Test {

assertThat(result.statusCode).isEqualTo(204)
}
}
}
74 changes: 74 additions & 0 deletions end2end-tests/openfeign/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
val fabrikt: Configuration by configurations.creating

val generationDir = "$buildDir/generated"
val apiFile = "${rootProject.projectDir}/src/test/resources/examples/okHttpClient/api.yaml"

sourceSets {
main { java.srcDirs("$generationDir/src/main/kotlin") }
test { java.srcDirs("$generationDir/src/test/kotlin") }
}

plugins {
id("org.jetbrains.kotlin.jvm") version "1.8.20" // Apply the Kotlin JVM plugin to add support for Kotlin.
}

java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

val jacksonVersion: String by rootProject.extra
val junitVersion: String by rootProject.extra

dependencies {
implementation("com.squareup.okhttp3:okhttp:4.10.0")
implementation("io.github.openfeign:feign-core:13.3")
implementation("io.github.openfeign:feign-jackson:13.3")
implementation("io.github.openfeign:feign-okhttp:13.3")
implementation("jakarta.validation:jakarta.validation-api:3.0.2")
implementation("javax.validation:validation-api:2.0.1.Final")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")
implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion")
implementation("com.fasterxml.jackson.core:jackson-annotations:$jacksonVersion")

testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion")
testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion")
testImplementation("org.assertj:assertj-core:3.24.2")
testImplementation("org.wiremock:wiremock:3.3.1")
testImplementation("com.marcinziolo:kotlin-wiremock:2.1.1")
}

tasks {

val generateCode by creating(JavaExec::class) {
inputs.files(apiFile)
outputs.dir(generationDir)
outputs.cacheIf { true }
classpath = rootProject.files("./build/libs/fabrikt-${rootProject.version}.jar")
mainClass.set("com.cjbooms.fabrikt.cli.CodeGen")
args = listOf(
"--output-directory", generationDir,
"--base-package", "com.example",
"--api-file", apiFile,
"--targets", "http_models",
"--targets", "client",
"--http-client-target", "open_feign",
)
dependsOn(":jar")
dependsOn(":shadowJar")
}

withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = "17"
dependsOn(generateCode)
}


withType<Test> {
useJUnitPlatform()
jvmArgs = listOf("--add-opens=java.base/java.lang=ALL-UNNAMED", "--add-opens=java.base/java.util=ALL-UNNAMED")

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.cjbooms.fabrikt.clients

import com.example.client.ExamplePath1Client
import com.example.models.FirstModel
import com.example.models.QueryResult
import com.fasterxml.jackson.databind.ObjectMapper
import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.common.ConsoleNotifier
import com.github.tomakehurst.wiremock.core.WireMockConfiguration
import com.marcinziolo.kotlin.wiremock.contains
import com.marcinziolo.kotlin.wiremock.get
import com.marcinziolo.kotlin.wiremock.like
import com.marcinziolo.kotlin.wiremock.returns
import feign.Feign
import feign.jackson.JacksonDecoder
import feign.jackson.JacksonEncoder
import feign.okhttp.OkHttpClient
import java.net.ServerSocket
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class AdditionalQueryParametersTest {
private val port: Int = ServerSocket(0).use { socket -> socket.localPort }
private val wiremock: WireMockServer = WireMockServer(
WireMockConfiguration.options().port(port).notifier(ConsoleNotifier(true)))
private val mapper = ObjectMapper()
private val examplePath1Client: ExamplePath1Client = Feign
.builder()
.client(OkHttpClient())
.encoder(JacksonEncoder(mapper))
.decoder(JacksonDecoder(mapper))
.target(ExamplePath1Client::class.java, "http://localhost:$port")

@BeforeEach
fun setUp() {
wiremock.start()
}

@AfterEach
fun afterEach() {
wiremock.resetAll()
wiremock.stop()
}

@Test
fun `additional query parameters are properly appended to requests`() {
val expectedResponse = QueryResult(listOf(FirstModel(id = "the parameter was there!")))
wiremock.get {
urlPath like "/example-path-1"
queryParams contains "unspecified_param" like "some_value"
} returns {
statusCode = 200
body = mapper.writeValueAsString(expectedResponse)
}
val result = examplePath1Client.getExamplePath1(additionalQueryParameters = mapOf("unspecified_param" to "some_value"))
Assertions.assertThat(result).isEqualTo(expectedResponse)
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ rootProject.name = "fabrikt"

include(
"end2end-tests:okhttp",
"end2end-tests:openfeign",
"end2end-tests:ktor",
"end2end-tests:models-jackson",
)
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ object ClientGeneratorUtils {
const val ACCEPT_HEADER_NAME = "Accept"
const val ACCEPT_HEADER_VARIABLE_NAME = "acceptHeader"
const val ADDITIONAL_HEADERS_PARAMETER_NAME = "additionalHeaders"
const val ADDITIONAL_QUERY_PARAMETERS_PARAMETER_NAME = "additionalQueryParameters"

/**
* Gives the Kotlin return type for an API call based on the Content-Types specified in the Operation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.cjbooms.fabrikt.generators.GeneratorUtils.toClassName
import com.cjbooms.fabrikt.generators.GeneratorUtils.toKdoc
import com.cjbooms.fabrikt.generators.TypeFactory
import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.ADDITIONAL_HEADERS_PARAMETER_NAME
import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.ADDITIONAL_QUERY_PARAMETERS_PARAMETER_NAME
import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.addIncomingParameters
import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.deriveClientParameters
import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.simpleClientName
Expand Down Expand Up @@ -66,6 +67,14 @@ class OkHttpSimpleClientGenerator(
.defaultValue("emptyMap()")
.build()
)
.addParameter(
ParameterSpec.builder(
ADDITIONAL_QUERY_PARAMETERS_PARAMETER_NAME,
TypeFactory.createMapOfStringToNonNullType(String::class.asTypeName())
)
.defaultValue("emptyMap()")
.build()
)
.addCode(
SimpleClientOperationStatement(
packages,
Expand Down Expand Up @@ -179,6 +188,7 @@ data class SimpleClientOperationStatement(
)
}
}
this.add("\n.also { builder -> additionalQueryParameters.forEach { builder.queryParam(it.key, it.value) } }")
return this.add("\n.build()\n")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.cjbooms.fabrikt.generators.GeneratorUtils.getPrimaryContentMediaType
import com.cjbooms.fabrikt.generators.GeneratorUtils.toKdoc
import com.cjbooms.fabrikt.generators.TypeFactory
import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.ADDITIONAL_HEADERS_PARAMETER_NAME
import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.ADDITIONAL_QUERY_PARAMETERS_PARAMETER_NAME
import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.addIncomingParameters
import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.deriveClientParameters
import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.getReturnType
Expand Down Expand Up @@ -95,6 +96,15 @@ class OpenFeignInterfaceGenerator(
.defaultValue("emptyMap()")
.build(),
)
.addParameter(
ParameterSpec.builder(
ADDITIONAL_QUERY_PARAMETERS_PARAMETER_NAME,
TypeFactory.createMapOfStringToNonNullType(String::class.asTypeName()),
)
.addAnnotation(OpenFeignAnnotations.QUERY_MAP)
.defaultValue("emptyMap()")
.build(),
)
.returns(
operation
.getReturnType(packages)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ object OpenFeignImports {

val HEADER_MAP = ClassName(Packages.FEIGN, "HeaderMap")

val QUERY_MAP = ClassName(Packages.FEIGN, "QueryMap")

val PARAM = ClassName(Packages.FEIGN, "Param")
}

Expand All @@ -24,6 +26,11 @@ object OpenFeignAnnotations {
.builder(OpenFeignImports.HEADER_MAP)
.build()

val QUERY_MAP: AnnotationSpec =
AnnotationSpec
.builder(OpenFeignImports.QUERY_MAP)
.build()

fun requestLineBuilder(): AnnotationSpec.Builder =
AnnotationSpec
.builder(OpenFeignImports.REQUEST_LINE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ public class HelloClient(
@Throws(ApiException::class)
public fun helloWorld(
parameter: ExternalParameter,
additionalHeaders: Map<String, String> =
emptyMap(),
additionalHeaders: Map<String, String> = emptyMap(),
additionalQueryParameters: Map<String, String> = emptyMap(),
): ApiResponse<ContainingExternalReference> {
val httpUrl: HttpUrl = "$baseUrl/hello"
.pathParam("{parameter}" to parameter)
.toHttpUrl()
.newBuilder()
.also { builder -> additionalQueryParameters.forEach { builder.queryParam(it.key, it.value) } }
.build()

val headerBuilder = Headers.Builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ public class ExamplePath1Client(
queryParam2: Int? = null,
acceptHeader: String = "application/vnd.custom.media+xml",
additionalHeaders: Map<String, String> = emptyMap(),
additionalQueryParameters: Map<String, String> = emptyMap(),
): ApiResponse<QueryResult> {
val httpUrl: HttpUrl = "$baseUrl/example-path-1"
.toHttpUrl()
.newBuilder()
.queryParam("explode_list_query_param", explodeListQueryParam, true)
.queryParam("query_param2", queryParam2)
.also { builder -> additionalQueryParameters.forEach { builder.queryParam(it.key, it.value) } }
.build()

val headerBuilder = Headers.Builder()
Expand Down Expand Up @@ -79,12 +81,14 @@ public class ExamplePath2Client(
queryParam2: Int? = null,
accept: ContentType? = null,
additionalHeaders: Map<String, String> = emptyMap(),
additionalQueryParameters: Map<String, String> = emptyMap(),
): ApiResponse<QueryResult> {
val httpUrl: HttpUrl = "$baseUrl/example-path-2"
.toHttpUrl()
.newBuilder()
.queryParam("explode_list_query_param", explodeListQueryParam, true)
.queryParam("query_param2", queryParam2)
.also { builder -> additionalQueryParameters.forEach { builder.queryParam(it.key, it.value) } }
.build()

val headerBuilder = Headers.Builder()
Expand Down Expand Up @@ -117,10 +121,12 @@ public class MultipleResponseSchemasClient(
public fun getMultipleResponseSchemas(
accept: ContentType? = null,
additionalHeaders: Map<String, String> = emptyMap(),
additionalQueryParameters: Map<String, String> = emptyMap(),
): ApiResponse<JsonNode> {
val httpUrl: HttpUrl = "$baseUrl/multiple-response-schemas"
.toHttpUrl()
.newBuilder()
.also { builder -> additionalQueryParameters.forEach { builder.queryParam(it.key, it.value) } }
.build()

val headerBuilder = Headers.Builder()
Expand Down Expand Up @@ -151,10 +157,12 @@ public class DifferentSuccessAndErrorResponseSchemaClient(
public fun getDifferentSuccessAndErrorResponseSchema(
additionalHeaders: Map<String, String> =
emptyMap(),
additionalQueryParameters: Map<String, String> = emptyMap(),
): ApiResponse<SuccessResponse> {
val httpUrl: HttpUrl = "$baseUrl/different-success-and-error-response-schema"
.toHttpUrl()
.newBuilder()
.also { builder -> additionalQueryParameters.forEach { builder.queryParam(it.key, it.value) } }
.build()

val headerBuilder = Headers.Builder()
Expand Down
Loading

0 comments on commit 10c5a67

Please sign in to comment.