diff --git a/build.gradle.kts b/build.gradle.kts index b32f4953..a5577f1c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -77,7 +77,7 @@ tasks { attributes["Built-JDK"] = System.getProperty("java.version") attributes["Built-Gradle"] = gradle.gradleVersion } - archiveBaseName.set(executableName) + archiveFileName.set(executableName + ".jar") archiveClassifier.set("") } @@ -175,3 +175,9 @@ signing { sign(publishing.publications["fabrikt"]) } + +tasks { + test { + jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED") + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 12d38de6..070cb702 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/MicronautControllerInterfaceGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/MicronautControllerInterfaceGenerator.kt index 839cac15..f526dc3a 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/MicronautControllerInterfaceGenerator.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/controller/MicronautControllerInterfaceGenerator.kt @@ -274,7 +274,7 @@ class MicronautControllerInterfaceGenerator( val systemRoleTypeName = ClassName("de.flipnext.core.usermanagement.users.domain", "SystemRole") val requirements = - op.securityRequirements.getOrNull(0)?.requirements?.values?.first()?.parameters + op.securityRequirements.getOrNull(0)?.requirements?.values?.firstOrNull()?.parameters val spec = AnnotationSpec.builder(MicronautImports.SECURED) // **warning** this is a hack to get the system role names into the annotation // since this is already custom flip code we use the system role name directly diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/MicronautControllerGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/MicronautControllerGeneratorTest.kt index 6e60ca75..24b69c4c 100644 --- a/src/test/kotlin/com/cjbooms/fabrikt/generators/MicronautControllerGeneratorTest.kt +++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/MicronautControllerGeneratorTest.kt @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource +import java.io.File import java.util.stream.Stream @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -64,6 +65,13 @@ class MicronautControllerGeneratorTest { api, ).generate().toSingleFile() + /* + Use this to update the expected output + File("src/test/resources/examples/$testCaseName/controllers/micronaut/Controllers.kt").writeText( + controllers + ) + */ + assertThat(controllers).isEqualTo(expectedControllers) } @@ -118,6 +126,7 @@ class MicronautControllerGeneratorTest { assertThat(controllerAnnotations).containsOnly( "io.micronaut.http.annotation.Controller", + "io.micronaut.validation.Validated", ) } diff --git a/src/test/resources/examples/authentication/controllers/micronaut/Controllers.kt b/src/test/resources/examples/authentication/controllers/micronaut/Controllers.kt index a488db20..2f2edfbb 100644 --- a/src/test/resources/examples/authentication/controllers/micronaut/Controllers.kt +++ b/src/test/resources/examples/authentication/controllers/micronaut/Controllers.kt @@ -7,11 +7,15 @@ import io.micronaut.http.annotation.QueryValue import io.micronaut.security.annotation.Secured import io.micronaut.security.authentication.Authentication import io.micronaut.security.rules.SecurityRule +import io.micronaut.validation.Validated import kotlin.String import kotlin.Unit @Controller -interface RequiredController { +@Validated +class RequiredController( + val testPathDelegate: TestPathDelegate +) { /** * * @@ -22,11 +26,21 @@ interface RequiredController { fun testPath( @QueryValue(value = "testString") testString: String, authentication: Authentication - ): HttpResponse + ): HttpResponse = testPathDelegate.testPath( + testString, + authentication + ) + + interface TestPathDelegate { + fun testPath(testString: String, authentication: Authentication): HttpResponse + } } @Controller -interface ProhibitedController { +@Validated +class ProhibitedController( + val testPathDelegate: TestPathDelegate +) { /** * * @@ -34,11 +48,19 @@ interface ProhibitedController { */ @Get(uri = "/prohibited") @Secured(SecurityRule.IS_ANONYMOUS) - fun testPath(@QueryValue(value = "testString") testString: String): HttpResponse + fun testPath(@QueryValue(value = "testString") testString: String): HttpResponse = + testPathDelegate.testPath(testString) + + interface TestPathDelegate { + fun testPath(testString: String): HttpResponse + } } @Controller -interface OptionalController { +@Validated +class OptionalController( + val testPathDelegate: TestPathDelegate +) { /** * * @@ -49,22 +71,40 @@ interface OptionalController { fun testPath( @QueryValue(value = "testString") testString: String, authentication: Authentication? - ): HttpResponse + ): HttpResponse = testPathDelegate.testPath( + testString, + authentication + ) + + interface TestPathDelegate { + fun testPath(testString: String, authentication: Authentication?): HttpResponse + } } @Controller -interface NoneController { +@Validated +class NoneController( + val testPathDelegate: TestPathDelegate +) { /** * * * @param testString */ @Get(uri = "/none") - fun testPath(@QueryValue(value = "testString") testString: String): HttpResponse + fun testPath(@QueryValue(value = "testString") testString: String): HttpResponse = + testPathDelegate.testPath(testString) + + interface TestPathDelegate { + fun testPath(testString: String): HttpResponse + } } @Controller -interface DefaultController { +@Validated +class DefaultController( + val testPathDelegate: TestPathDelegate +) { /** * * @@ -75,5 +115,12 @@ interface DefaultController { fun testPath( @QueryValue(value = "testString") testString: String, authentication: Authentication - ): HttpResponse + ): HttpResponse = testPathDelegate.testPath( + testString, + authentication + ) + + interface TestPathDelegate { + fun testPath(testString: String, authentication: Authentication): HttpResponse + } } diff --git a/src/test/resources/examples/githubApi/controllers/micronaut/Controllers.kt b/src/test/resources/examples/githubApi/controllers/micronaut/Controllers.kt index 69f78711..6c4f1c1e 100644 --- a/src/test/resources/examples/githubApi/controllers/micronaut/Controllers.kt +++ b/src/test/resources/examples/githubApi/controllers/micronaut/Controllers.kt @@ -23,6 +23,7 @@ import io.micronaut.http.annotation.Post import io.micronaut.http.annotation.Produces import io.micronaut.http.annotation.Put import io.micronaut.http.annotation.QueryValue +import io.micronaut.validation.Validated import java.util.UUID import javax.validation.Valid import javax.validation.constraints.Max @@ -34,7 +35,10 @@ import kotlin.Unit import kotlin.collections.List @Controller -interface InternalEventsController { +@Validated +class InternalEventsController( + val postDelegate: PostDelegate +) { /** * Generate change events for a list of entities * @@ -46,11 +50,22 @@ interface InternalEventsController { fun post( @Body @Valid bulkEntityDetails: BulkEntityDetails - ): HttpResponse + ): HttpResponse = + postDelegate.post(bulkEntityDetails) + + interface PostDelegate { + fun post(bulkEntityDetails: BulkEntityDetails): HttpResponse + } } @Controller -interface ContributorsController { +@Validated +class ContributorsController( + val searchContributorsDelegate: SearchContributorsDelegate, + val createContributorDelegate: CreateContributorDelegate, + val getContributorDelegate: GetContributorDelegate, + val putByIdDelegate: PutByIdDelegate +) { /** * Page through all the Contributor resources matching the query filters * @@ -74,7 +89,12 @@ interface ContributorsController { @Header(value = "X-Flow-Id") xFlowId: String?, @QueryValue(value = "include_inactive") includeInactive: Boolean?, @QueryValue(value = "cursor") cursor: String? - ): HttpResponse + ): HttpResponse = searchContributorsDelegate.searchContributors( + limit, + xFlowId, + includeInactive, + cursor + ) /** * Create a new Contributor @@ -99,7 +119,11 @@ interface ContributorsController { contributor: Contributor, @Header(value = "X-Flow-Id") xFlowId: String?, @Header(value = "Idempotency-Key") idempotencyKey: UUID? - ): HttpResponse + ): HttpResponse = createContributorDelegate.createContributor( + contributor, + xFlowId, + idempotencyKey + ) /** * Get a Contributor by ID @@ -123,7 +147,12 @@ interface ContributorsController { @QueryValue(value = "status", defaultValue = "all") status: StatusQueryParam, @Header(value = "X-Flow-Id") xFlowId: String?, @Header(value = "If-None-Match") ifNoneMatch: String? - ): HttpResponse + ): HttpResponse = getContributorDelegate.getContributor( + id, + status, + xFlowId, + ifNoneMatch + ) /** * Update an existing Contributor @@ -156,11 +185,53 @@ interface ContributorsController { @Header(value = "If-Match") ifMatch: String, @Header(value = "X-Flow-Id") xFlowId: String?, @Header(value = "Idempotency-Key") idempotencyKey: UUID? - ): HttpResponse + ): HttpResponse = putByIdDelegate.putById(contributor, id, ifMatch, xFlowId, idempotencyKey) + + interface SearchContributorsDelegate { + fun searchContributors( + limit: Int, + xFlowId: String?, + includeInactive: Boolean?, + cursor: String? + ): HttpResponse + } + + interface CreateContributorDelegate { + fun createContributor( + contributor: Contributor, + xFlowId: String?, + idempotencyKey: UUID? + ): HttpResponse + } + + interface GetContributorDelegate { + fun getContributor( + id: String, + status: StatusQueryParam, + xFlowId: String?, + ifNoneMatch: String? + ): HttpResponse + } + + interface PutByIdDelegate { + fun putById( + contributor: Contributor, + id: String, + ifMatch: String, + xFlowId: String?, + idempotencyKey: UUID? + ): HttpResponse + } } @Controller -interface OrganisationsController { +@Validated +class OrganisationsController( + val getDelegate: GetDelegate, + val postDelegate: PostDelegate, + val getByIdDelegate: GetByIdDelegate, + val putByIdDelegate: PutByIdDelegate +) { /** * Page through all the Organisation resources matching the query filters * @@ -184,7 +255,12 @@ interface OrganisationsController { @Header(value = "X-Flow-Id") xFlowId: String?, @QueryValue(value = "include_inactive") includeInactive: Boolean?, @QueryValue(value = "cursor") cursor: String? - ): HttpResponse + ): HttpResponse = getDelegate.get( + limit, + xFlowId, + includeInactive, + cursor + ) /** * Create a new Organisation @@ -209,7 +285,7 @@ interface OrganisationsController { organisation: Organisation, @Header(value = "X-Flow-Id") xFlowId: String?, @Header(value = "Idempotency-Key") idempotencyKey: UUID? - ): HttpResponse + ): HttpResponse = postDelegate.post(organisation, xFlowId, idempotencyKey) /** * Get a Organisation by ID @@ -233,7 +309,7 @@ interface OrganisationsController { @QueryValue(value = "status", defaultValue = "all") status: StatusQueryParam, @Header(value = "X-Flow-Id") xFlowId: String?, @Header(value = "If-None-Match") ifNoneMatch: String? - ): HttpResponse + ): HttpResponse = getByIdDelegate.getById(id, status, xFlowId, ifNoneMatch) /** * Update an existing Organisation @@ -266,11 +342,59 @@ interface OrganisationsController { @Header(value = "If-Match") ifMatch: String, @Header(value = "X-Flow-Id") xFlowId: String?, @Header(value = "Idempotency-Key") idempotencyKey: UUID? - ): HttpResponse + ): HttpResponse = putByIdDelegate.putById( + organisation, + id, + ifMatch, + xFlowId, + idempotencyKey + ) + + interface GetDelegate { + fun get( + limit: Int, + xFlowId: String?, + includeInactive: Boolean?, + cursor: String? + ): HttpResponse + } + + interface PostDelegate { + fun post( + organisation: Organisation, + xFlowId: String?, + idempotencyKey: UUID? + ): HttpResponse + } + + interface GetByIdDelegate { + fun getById( + id: String, + status: StatusQueryParam, + xFlowId: String?, + ifNoneMatch: String? + ): HttpResponse + } + + interface PutByIdDelegate { + fun putById( + organisation: Organisation, + id: String, + ifMatch: String, + xFlowId: String?, + idempotencyKey: UUID? + ): HttpResponse + } } @Controller -interface OrganisationsContributorsController { +@Validated +class OrganisationsContributorsController( + val getDelegate: GetDelegate, + val getByIdDelegate: GetByIdDelegate, + val putByIdDelegate: PutByIdDelegate, + val deleteByIdDelegate: DeleteByIdDelegate +) { /** * Page through all the Contributor resources for this parent Organisation matching the query * filters @@ -297,7 +421,13 @@ interface OrganisationsContributorsController { @Header(value = "X-Flow-Id") xFlowId: String?, @QueryValue(value = "include_inactive") includeInactive: Boolean?, @QueryValue(value = "cursor") cursor: String? - ): HttpResponse + ): HttpResponse = getDelegate.get( + parentId, + limit, + xFlowId, + includeInactive, + cursor + ) /** * Get a Contributor for this Organisation by ID @@ -319,7 +449,7 @@ interface OrganisationsContributorsController { @PathVariable(value = "id") id: String, @Header(value = "X-Flow-Id") xFlowId: String?, @Header(value = "If-None-Match") ifNoneMatch: String? - ): HttpResponse + ): HttpResponse = getByIdDelegate.getById(parentId, id, xFlowId, ifNoneMatch) /** * Add an existing Contributor to this Organisation @@ -350,7 +480,7 @@ interface OrganisationsContributorsController { @Header(value = "If-Match") ifMatch: String, @Header(value = "X-Flow-Id") xFlowId: String?, @Header(value = "Idempotency-Key") idempotencyKey: UUID? - ): HttpResponse + ): HttpResponse = putByIdDelegate.putById(parentId, id, ifMatch, xFlowId, idempotencyKey) /** * Remove Contributor from this Organisation. Does not delete the underlying Contributor. @@ -366,11 +496,54 @@ interface OrganisationsContributorsController { @PathVariable(value = "parent-id") parentId: String, @PathVariable(value = "id") id: String, @Header(value = "X-Flow-Id") xFlowId: String? - ): HttpResponse + ): HttpResponse = deleteByIdDelegate.deleteById(parentId, id, xFlowId) + + interface GetDelegate { + fun get( + parentId: String, + limit: Int, + xFlowId: String?, + includeInactive: Boolean?, + cursor: String? + ): HttpResponse + } + + interface GetByIdDelegate { + fun getById( + parentId: String, + id: String, + xFlowId: String?, + ifNoneMatch: String? + ): HttpResponse + } + + interface PutByIdDelegate { + fun putById( + parentId: String, + id: String, + ifMatch: String, + xFlowId: String?, + idempotencyKey: UUID? + ): HttpResponse + } + + interface DeleteByIdDelegate { + fun deleteById( + parentId: String, + id: String, + xFlowId: String? + ): HttpResponse + } } @Controller -interface RepositoriesController { +@Validated +class RepositoriesController( + val getDelegate: GetDelegate, + val postDelegate: PostDelegate, + val getByIdDelegate: GetByIdDelegate, + val putByIdDelegate: PutByIdDelegate +) { /** * Page through all the Repository resources matching the query filters * @@ -402,7 +575,14 @@ interface RepositoriesController { name: List?, @QueryValue(value = "include_inactive") includeInactive: Boolean?, @QueryValue(value = "cursor") cursor: String? - ): HttpResponse + ): HttpResponse = getDelegate.get( + limit, + xFlowId, + slug, + name, + includeInactive, + cursor + ) /** * Create a new Repository @@ -427,7 +607,7 @@ interface RepositoriesController { repository: Repository, @Header(value = "X-Flow-Id") xFlowId: String?, @Header(value = "Idempotency-Key") idempotencyKey: UUID? - ): HttpResponse + ): HttpResponse = postDelegate.post(repository, xFlowId, idempotencyKey) /** * Get a Repository by ID @@ -451,7 +631,7 @@ interface RepositoriesController { @QueryValue(value = "status", defaultValue = "all") status: StatusQueryParam, @Header(value = "X-Flow-Id") xFlowId: String?, @Header(value = "If-None-Match") ifNoneMatch: String? - ): HttpResponse + ): HttpResponse = getByIdDelegate.getById(id, status, xFlowId, ifNoneMatch) /** * Update an existing Repository @@ -484,11 +664,55 @@ interface RepositoriesController { @Header(value = "If-Match") ifMatch: String, @Header(value = "X-Flow-Id") xFlowId: String?, @Header(value = "Idempotency-Key") idempotencyKey: UUID? - ): HttpResponse + ): HttpResponse = putByIdDelegate.putById(repository, id, ifMatch, xFlowId, idempotencyKey) + + interface GetDelegate { + fun get( + limit: Int, + xFlowId: String?, + slug: List?, + name: List?, + includeInactive: Boolean?, + cursor: String? + ): HttpResponse + } + + interface PostDelegate { + fun post( + repository: Repository, + xFlowId: String?, + idempotencyKey: UUID? + ): HttpResponse + } + + interface GetByIdDelegate { + fun getById( + id: String, + status: StatusQueryParam, + xFlowId: String?, + ifNoneMatch: String? + ): HttpResponse + } + + interface PutByIdDelegate { + fun putById( + repository: Repository, + id: String, + ifMatch: String, + xFlowId: String?, + idempotencyKey: UUID? + ): HttpResponse + } } @Controller -interface RepositoriesPullRequestsController { +@Validated +class RepositoriesPullRequestsController( + val getDelegate: GetDelegate, + val postDelegate: PostDelegate, + val getByIdDelegate: GetByIdDelegate, + val putByIdDelegate: PutByIdDelegate +) { /** * Page through all the PullRequest resources for this parent Repository matching the query * filters @@ -515,7 +739,13 @@ interface RepositoriesPullRequestsController { @Header(value = "X-Flow-Id") xFlowId: String?, @QueryValue(value = "include_inactive") includeInactive: Boolean?, @QueryValue(value = "cursor") cursor: String? - ): HttpResponse + ): HttpResponse = getDelegate.get( + parentId, + limit, + xFlowId, + includeInactive, + cursor + ) /** * Create a new PullRequest for this parent Repository @@ -542,7 +772,7 @@ interface RepositoriesPullRequestsController { @PathVariable(value = "parent-id") parentId: String, @Header(value = "X-Flow-Id") xFlowId: String?, @Header(value = "Idempotency-Key") idempotencyKey: UUID? - ): HttpResponse + ): HttpResponse = postDelegate.post(pullRequest, parentId, xFlowId, idempotencyKey) /** * Get a PullRequest for this Repository by ID @@ -564,7 +794,7 @@ interface RepositoriesPullRequestsController { @PathVariable(value = "id") id: String, @Header(value = "X-Flow-Id") xFlowId: String?, @Header(value = "If-None-Match") ifNoneMatch: String? - ): HttpResponse + ): HttpResponse = getByIdDelegate.getById(parentId, id, xFlowId, ifNoneMatch) /** * Update the PullRequest owned by this Repository @@ -599,5 +829,51 @@ interface RepositoriesPullRequestsController { @Header(value = "If-Match") ifMatch: String, @Header(value = "X-Flow-Id") xFlowId: String?, @Header(value = "Idempotency-Key") idempotencyKey: UUID? - ): HttpResponse + ): HttpResponse = putByIdDelegate.putById( + pullRequest, + parentId, + id, + ifMatch, + xFlowId, + idempotencyKey + ) + + interface GetDelegate { + fun get( + parentId: String, + limit: Int, + xFlowId: String?, + includeInactive: Boolean?, + cursor: String? + ): HttpResponse + } + + interface PostDelegate { + fun post( + pullRequest: PullRequest, + parentId: String, + xFlowId: String?, + idempotencyKey: UUID? + ): HttpResponse + } + + interface GetByIdDelegate { + fun getById( + parentId: String, + id: String, + xFlowId: String?, + ifNoneMatch: String? + ): HttpResponse + } + + interface PutByIdDelegate { + fun putById( + pullRequest: PullRequest, + parentId: String, + id: String, + ifMatch: String, + xFlowId: String?, + idempotencyKey: UUID? + ): HttpResponse + } } diff --git a/src/test/resources/examples/parameterNameClash/controllers/micronaut/Controllers.kt b/src/test/resources/examples/parameterNameClash/controllers/micronaut/Controllers.kt index b9de8fc5..fb017717 100644 --- a/src/test/resources/examples/parameterNameClash/controllers/micronaut/Controllers.kt +++ b/src/test/resources/examples/parameterNameClash/controllers/micronaut/Controllers.kt @@ -9,12 +9,17 @@ import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.PathVariable import io.micronaut.http.annotation.Post import io.micronaut.http.annotation.QueryValue +import io.micronaut.validation.Validated import javax.validation.Valid import kotlin.String import kotlin.Unit @Controller -interface ExampleController { +@Validated +class ExampleController( + val getByIdDelegate: GetByIdDelegate, + val postDelegate: PostDelegate +) { /** * * @@ -26,7 +31,7 @@ interface ExampleController { @PathVariable(value = "pathB") pathB: String, @QueryValue(value = "queryB") queryB: String - ): HttpResponse + ): HttpResponse = getByIdDelegate.getById(pathB, queryB) /** * @@ -41,5 +46,16 @@ interface ExampleController { bodySomeObject: SomeObject, @QueryValue(value = "querySomeObject") querySomeObject: String - ): HttpResponse + ): HttpResponse = postDelegate.post( + bodySomeObject, + querySomeObject + ) + + interface GetByIdDelegate { + fun getById(pathB: String, queryB: String): HttpResponse + } + + interface PostDelegate { + fun post(bodySomeObject: SomeObject, querySomeObject: String): HttpResponse + } } diff --git a/src/test/resources/examples/pathLevelParameters/controllers/micronaut/Controllers.kt b/src/test/resources/examples/pathLevelParameters/controllers/micronaut/Controllers.kt index cf52096e..bf5360a7 100644 --- a/src/test/resources/examples/pathLevelParameters/controllers/micronaut/Controllers.kt +++ b/src/test/resources/examples/pathLevelParameters/controllers/micronaut/Controllers.kt @@ -4,11 +4,15 @@ import io.micronaut.http.HttpResponse import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.QueryValue +import io.micronaut.validation.Validated import kotlin.String import kotlin.Unit @Controller -interface ExampleController { +@Validated +class ExampleController( + val getDelegate: GetDelegate +) { /** * * @@ -17,5 +21,9 @@ interface ExampleController { */ @Get(uri = "/example") fun get(@QueryValue(value = "a") a: String, @QueryValue(value = "b") b: String): - HttpResponse + HttpResponse = getDelegate.get(a, b) + + interface GetDelegate { + fun get(a: String, b: String): HttpResponse + } } diff --git a/src/test/resources/examples/singleAllOf/controllers/micronaut/Controllers.kt b/src/test/resources/examples/singleAllOf/controllers/micronaut/Controllers.kt index bdda9488..f587e5ea 100644 --- a/src/test/resources/examples/singleAllOf/controllers/micronaut/Controllers.kt +++ b/src/test/resources/examples/singleAllOf/controllers/micronaut/Controllers.kt @@ -5,13 +5,21 @@ import io.micronaut.http.HttpResponse import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Produces +import io.micronaut.validation.Validated @Controller -interface TestController { +@Validated +class TestController( + val testDelegate: TestDelegate +) { /** * */ @Get(uri = "/test") @Produces(value = ["application/json"]) - fun test(): HttpResponse + fun test(): HttpResponse = testDelegate.test() + + interface TestDelegate { + fun test(): HttpResponse + } }