Skip to content

Commit d61af26

Browse files
committed
refactor: allow cumulative update of response behaviour between handlers.
1 parent 5ce2ed2 commit d61af26

File tree

9 files changed

+168
-31
lines changed

9 files changed

+168
-31
lines changed

core/api/src/main/java/io/gatehill/imposter/http/DefaultResponseBehaviourFactory.kt

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,22 @@ import com.google.common.base.Strings
4646
import io.gatehill.imposter.plugin.config.resource.BasicResourceConfig
4747
import io.gatehill.imposter.script.ReadWriteResponseBehaviour
4848
import io.gatehill.imposter.script.ReadWriteResponseBehaviourImpl
49+
import io.gatehill.imposter.script.ResponseBehaviour
50+
import io.gatehill.imposter.util.ResourceUtil
4951

5052
/**
5153
* @author Pete Cornish
5254
*/
5355
open class DefaultResponseBehaviourFactory protected constructor() : ResponseBehaviourFactory {
54-
override fun build(statusCode: Int, resourceConfig: BasicResourceConfig): ReadWriteResponseBehaviour {
55-
val responseBehaviour = ReadWriteResponseBehaviourImpl()
56+
override fun build(
57+
statusCode: Int,
58+
resourceConfig: BasicResourceConfig,
59+
exchangeState: HttpExchangeState,
60+
): ReadWriteResponseBehaviourImpl {
61+
// previous handler(s) may have set response behaviour
62+
val responseBehaviour = exchangeState.getOrPut(ResourceUtil.RC_RESPONSE_BEHAVIOUR) {
63+
return@getOrPut ReadWriteResponseBehaviourImpl()
64+
}
5665
populate(statusCode, resourceConfig, responseBehaviour)
5766
return responseBehaviour
5867
}
@@ -92,7 +101,34 @@ open class DefaultResponseBehaviourFactory protected constructor() : ResponseBeh
92101
responseConfig.headers?.let { headers -> responseBehaviour.responseHeaders.putAll(headers) }
93102
}
94103

104+
override fun merge(
105+
source: ResponseBehaviour,
106+
target: ReadWriteResponseBehaviour
107+
) {
108+
if (source.statusCode != 0) {
109+
target.withStatusCode(source.statusCode)
110+
}
111+
if (!Strings.isNullOrEmpty(source.responseFile)) {
112+
target.withFile(source.responseFile!!)
113+
}
114+
if (!Strings.isNullOrEmpty(source.content)) {
115+
target.withContent(source.content)
116+
}
117+
if (source.isTemplate) {
118+
target.template()
119+
}
120+
if (source.performanceSimulation != null) {
121+
target.withPerformance(source.performanceSimulation)
122+
}
123+
if (source.failureType != null) {
124+
target.withFailureType(source.failureType)
125+
}
126+
if (source.responseHeaders.isNotEmpty()) {
127+
target.responseHeaders.putAll(source.responseHeaders)
128+
}
129+
}
130+
95131
companion object {
96132
val instance = DefaultResponseBehaviourFactory()
97133
}
98-
}
134+
}

core/api/src/main/java/io/gatehill/imposter/http/ResponseBehaviourFactory.kt

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,38 @@ package io.gatehill.imposter.http
4545
import io.gatehill.imposter.plugin.config.resource.BasicResourceConfig
4646
import io.gatehill.imposter.plugin.config.resource.ResponseConfig
4747
import io.gatehill.imposter.script.ReadWriteResponseBehaviour
48+
import io.gatehill.imposter.script.ResponseBehaviour
4849

4950
/**
5051
* @author Pete Cornish
5152
*/
5253
interface ResponseBehaviourFactory {
53-
fun build(statusCode: Int, resourceConfig: BasicResourceConfig): ReadWriteResponseBehaviour
54+
fun build(
55+
statusCode: Int,
56+
resourceConfig: BasicResourceConfig,
57+
exchangeState: HttpExchangeState,
58+
): ReadWriteResponseBehaviour
5459

5560
/**
5661
* Sets (but does not overwrite) values on the [io.gatehill.imposter.script.ResponseBehaviour], from
5762
* those on [ResponseConfig].
5863
*
5964
* @param statusCode the status code
60-
* @param responseConfig
65+
* @param resourceConfig
6166
* @param responseBehaviour
6267
*/
63-
fun populate(statusCode: Int, resourceConfig: BasicResourceConfig, responseBehaviour: ReadWriteResponseBehaviour)
64-
}
68+
fun populate(
69+
statusCode: Int,
70+
resourceConfig: BasicResourceConfig,
71+
responseBehaviour: ReadWriteResponseBehaviour
72+
)
73+
74+
/**
75+
* Merge values from the source [ResponseBehaviour] (if they are set)
76+
* on to the target [ResponseBehaviour].
77+
*/
78+
fun merge(
79+
source: ResponseBehaviour,
80+
target: ReadWriteResponseBehaviour
81+
)
82+
}

core/api/src/main/java/io/gatehill/imposter/util/ResourceUtil.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ object ResourceUtil {
5757
const val RC_LAST_HANDLER_TYPE = "handler.type"
5858
const val RESOURCE_CONFIG_KEY = "io.gatehill.imposter.resourceConfig"
5959
const val RC_REQUEST_ID_KEY = "request.id"
60+
const val RC_RESPONSE_BEHAVIOUR = "response.behaviour"
6061
const val RC_SEND_NOT_FOUND_RESPONSE = "response.sendNotFoundResponse"
6162

6263
/**

core/engine/src/main/java/io/gatehill/imposter/model/steps/RemoteProcessingStep.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,13 @@ class RemoteProcessingStep(
9292
ctx.config.capture?.forEach { (key, config) ->
9393
captureService.captureItem(key, config, remoteExchange, evaluators)
9494
}
95-
responseBehaviourFactory.build(statusCode, ctx.resourceConfig)
95+
responseBehaviourFactory.build(statusCode, ctx.resourceConfig, httpExchange)
9696
} catch (e: Exception) {
9797
logger.error("Error sending remote request: {} {}", ctx.config.method, ctx.config.url, e)
9898
val emptyResourceConfig = object : AbstractResourceConfig() {
9999
override val responseConfig = ResponseConfig()
100100
}
101-
responseBehaviourFactory.build(HttpUtil.HTTP_INTERNAL_ERROR, emptyResourceConfig)
101+
responseBehaviourFactory.build(HttpUtil.HTTP_INTERNAL_ERROR, emptyResourceConfig, httpExchange)
102102
}
103103
}
104104
}

core/engine/src/main/java/io/gatehill/imposter/service/ResponseRoutingServiceImpl.kt

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,11 @@ class ResponseRoutingServiceImpl @Inject constructor(
128128
checkNotNull(responseConfig) { "Response configuration must not be null" }
129129

130130
val statusCode = statusCodeFactory.calculateStatus(resourceConfig)
131-
val responseBehaviour: ReadWriteResponseBehaviour
131+
var responseBehaviour: ReadWriteResponseBehaviour = responseBehaviourFactory.build(
132+
statusCode,
133+
resourceConfig,
134+
httpExchange,
135+
)
132136

133137
val steps = stepService.determineSteps(pluginConfig, resourceConfig)
134138
if (logger.isTraceEnabled) {
@@ -142,7 +146,6 @@ class ResponseRoutingServiceImpl @Inject constructor(
142146
LogUtil.describeRequestShort(httpExchange),
143147
)
144148
}
145-
responseBehaviour = responseBehaviourFactory.build(statusCode, resourceConfig)
146149
} else {
147150
val responseBehaviours = steps.map {
148151
it.step.execute(
@@ -153,8 +156,17 @@ class ResponseRoutingServiceImpl @Inject constructor(
153156
additionalContext,
154157
)
155158
}
159+
156160
// only the last response behaviour is used
157-
responseBehaviour = responseBehaviours.last()
161+
val lastResponseBehaviour = responseBehaviours.last()
162+
if (lastResponseBehaviour.behaviourType == ResponseBehaviourType.SHORT_CIRCUIT) {
163+
responseBehaviour = lastResponseBehaviour
164+
} else {
165+
responseBehaviourFactory.merge(
166+
lastResponseBehaviour,
167+
responseBehaviour,
168+
)
169+
}
158170
}
159171

160172
// explicitly check if the root resource should have its response config used as defaults for its child resources

core/http/src/main/java/io/gatehill/imposter/http/HttpExchange.kt

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ package io.gatehill.imposter.http
4545
/**
4646
* @author Pete Cornish
4747
*/
48-
interface HttpExchange {
48+
interface HttpExchange : HttpExchangeState {
4949
var phase: ExchangePhase
5050
val request: HttpRequest
5151
val response: HttpResponse
@@ -62,23 +62,5 @@ interface HttpExchange {
6262
fun fail(statusCode: Int, cause: Throwable?)
6363
val failureCause: Throwable?
6464

65-
fun <T> get(key: String): T?
66-
fun put(key: String, value: Any)
67-
68-
fun <T : Any> getOrPut(key: String, defaultSupplier: () -> T): T {
69-
return get(key) ?: run {
70-
val value = defaultSupplier()
71-
put(key, value)
72-
return@run value
73-
}
74-
}
75-
76-
/**
77-
* Like [getOrPut] but returns [Unit].
78-
*/
79-
fun <T: Any> putIfAbsent(key: String, supplier: () -> T) {
80-
getOrPut(key, supplier)
81-
}
82-
8365
fun next()
8466
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (c) 2025.
3+
*
4+
* This file is part of Imposter.
5+
*
6+
* "Commons Clause" License Condition v1.0
7+
*
8+
* The Software is provided to you by the Licensor under the License, as
9+
* defined below, subject to the following condition.
10+
*
11+
* Without limiting other conditions in the License, the grant of rights
12+
* under the License will not include, and the License does not grant to
13+
* you, the right to Sell the Software.
14+
*
15+
* For purposes of the foregoing, "Sell" means practicing any or all of
16+
* the rights granted to you under the License to provide to third parties,
17+
* for a fee or other consideration (including without limitation fees for
18+
* hosting or consulting/support services related to the Software), a
19+
* product or service whose value derives, entirely or substantially, from
20+
* the functionality of the Software. Any license notice or attribution
21+
* required by the License must also include this Commons Clause License
22+
* Condition notice.
23+
*
24+
* Software: Imposter
25+
*
26+
* License: GNU Lesser General Public License version 3
27+
*
28+
* Licensor: Peter Cornish
29+
*
30+
* Imposter is free software: you can redistribute it and/or modify
31+
* it under the terms of the GNU Lesser General Public License as published by
32+
* the Free Software Foundation, either version 3 of the License, or
33+
* (at your option) any later version.
34+
*
35+
* Imposter is distributed in the hope that it will be useful,
36+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
37+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
38+
* GNU Lesser General Public License for more details.
39+
*
40+
* You should have received a copy of the GNU Lesser General Public License
41+
* along with Imposter. If not, see <https://www.gnu.org/licenses/>.
42+
*/
43+
44+
package io.gatehill.imposter.http
45+
46+
/**
47+
* Holds state for an HTTP exchange.
48+
*/
49+
interface HttpExchangeState {
50+
fun <T> get(key: String): T?
51+
52+
fun put(key: String, value: Any)
53+
54+
fun <T : Any> getOrPut(key: String, defaultSupplier: () -> T): T = get(key) ?: run {
55+
val value = defaultSupplier()
56+
put(key, value)
57+
return@run value
58+
}
59+
60+
/**
61+
* Like [getOrPut] but returns [Unit].
62+
*/
63+
fun <T: Any> putIfAbsent(key: String, supplier: () -> T) {
64+
getOrPut(key, supplier)
65+
}
66+
}

mock/openapi/src/main/java/io/gatehill/imposter/plugin/openapi/http/OpenApiResponseBehaviourFactory.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import io.gatehill.imposter.http.DefaultResponseBehaviourFactory
4747
import io.gatehill.imposter.plugin.config.resource.BasicResourceConfig
4848
import io.gatehill.imposter.plugin.openapi.config.OpenApiResponseConfig
4949
import io.gatehill.imposter.script.ReadWriteResponseBehaviour
50+
import io.gatehill.imposter.script.ResponseBehaviour
5051

5152
/**
5253
* Extends base response behaviour population with specific
@@ -66,4 +67,14 @@ class OpenApiResponseBehaviourFactory : DefaultResponseBehaviourFactory() {
6667
responseBehaviour.withExampleName(configExampleName!!)
6768
}
6869
}
70+
71+
override fun merge(
72+
source: ResponseBehaviour,
73+
target: ReadWriteResponseBehaviour
74+
) {
75+
super.merge(source, target)
76+
if (!Strings.isNullOrEmpty(source.exampleName)) {
77+
target.withExampleName(source.exampleName!!)
78+
}
79+
}
6980
}

mock/soap/src/main/java/io/gatehill/imposter/plugin/soap/http/SoapResponseBehaviourFactory.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import io.gatehill.imposter.http.DefaultResponseBehaviourFactory
4646
import io.gatehill.imposter.plugin.config.resource.BasicResourceConfig
4747
import io.gatehill.imposter.plugin.soap.config.SoapResponseConfig
4848
import io.gatehill.imposter.script.ReadWriteResponseBehaviour
49+
import io.gatehill.imposter.script.ResponseBehaviour
4950

5051
/**
5152
* Extends base response behaviour population with specific
@@ -67,4 +68,14 @@ class SoapResponseBehaviourFactory : DefaultResponseBehaviourFactory() {
6768
// overrides the status code to 500
6869
super.populate(statusCode, resourceConfig, responseBehaviour)
6970
}
71+
72+
override fun merge(
73+
source: ResponseBehaviour,
74+
target: ReadWriteResponseBehaviour
75+
) {
76+
super.merge(source, target)
77+
if (source.soapFault) {
78+
target.withSoapFault()
79+
}
80+
}
7081
}

0 commit comments

Comments
 (0)