Skip to content

Commit 45029fa

Browse files
committed
Environment preserves the type for primitive values
Signed-off-by: Gopal S Akshintala <[email protected]>
1 parent 24e50d1 commit 45029fa

File tree

8 files changed

+88
-64
lines changed

8 files changed

+88
-64
lines changed

.idea/kotlinc.xml

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

MODULE.bazel.lock

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.adoc

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -678,10 +678,11 @@ CAUTION: The recommendation is not to add too much code in <<Pre-req and Post-re
678678
[#_mutable_environment]
679679
=== Mutable Environment
680680

681-
* Environment is the only mutable-shared state across step executions, which can be used for data passing between the consumer and the library.
682-
* This can be mutated (set key-value pairs) through <<Pre-req and Post-res scripts>> (using `pm.environment.set()`) and <<#_pre_step_and_post_step_hooks,Pre-Step /Post-Step Hooks>> (using the reference `rundown.mutableEnv`) during execution.
683-
* Mutable Environment is `Map<String, Object>` (in Java) or `Map<String, Any?>` (in Kotlin). If you store a POJO as the value, it gets serialized to JSON when used to replace a `{{variable-key}}` placeholder. While fetching a POJO value from `mutableEnv` in JVM code, you may use `mutableEnv.get()` to get the value as a POJO, or `mutableEnv.getAsString()` to read it as a serialized JSON string.
684-
* While serializing/deserializing, the <<#_type_safety_with_flexible_json_pojo_marshallingserialization_and_unmarshallingdeserialization,customer adapters configured>> via `Kick` config shall be used. You may supply more adapters if needed via `Kick` config.
681+
* Environment is the only mutable-shared state across step executions, which can be used for data passing between the consumer and the library and across steps during execution.
682+
* Use-cases to usually mutate are in <<Pre-req and Post-res scripts>> (using `pm.environment.set()`) and <<#_pre_step_and_post_step_hooks,Pre-Step /Post-Step Hooks>> (using the reference `rundown.mutableEnv`) during execution.
683+
* Mutable Environment is `Map<String, Object>` (in Java) or `Map<String, Any?>` (in Kotlin), so you can use it to store any type of values, not just limited to `String` type.
684+
685+
NOTE: If a `mutableEnv` entry holds a POJO value, the value in the entry gets mutated to a serialized JSON String inside the `mutableEnv`, before getting used for variable replacement. This also takes care of replacing any variable placeholders inside POJO attribute values recursively. Although we can, the value is not deserialized back to POJO for perf reasons. You can always <<_read_environment_value_into_strong_type, read a specific value back as POJO>>, as needed.
685686

686687
==== Read Mutable Environment as Postman Environment JSON format
687688

@@ -692,12 +693,16 @@ so you can copy and import that environment conveniently into Postman.
692693
TIP: You do NOT need to save the copied Postman environment JSON from the debugger into file.
693694
You can paste (kbd:[Ctrl+v]) directly in the Postman environment window
694695

696+
[#_read_environment_value_into_strong_type]
695697
==== Read Environment value into Strong type
696698

697699
You can read any value from mutableEnv as a Strong type using `rundown.mutableEnv.<TypeT>getTypedObj()`
698700
See it in action: `getTypedObj()`
699701
test from link:{testdir}/com/salesforce/revoman/output/postman/PostmanEnvironmentTest.java[PostmanEnvironmentTest]
700702

703+
TIP: While serializing/deserializing, the <<#_type_safety_with_flexible_json_pojo_marshallingserialization_and_unmarshallingdeserialization,customer adapters configured>>
704+
via `Kick` config shall be used.You may supply more adapters if needed via `Kick` config.
705+
701706
==== `pmEnvSnapshot` in each StepReport
702707

703708
Each StepReport also has a `pmEnvSnapshot` to assert if a step has executed as expected and compare snapshots from different steps to examine the execution progress.

src/integrationTest/java/com/salesforce/revoman/integration/pokemon/PokemonTest.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import static com.google.common.truth.Truth.assertThat;
1111
import static com.salesforce.revoman.input.config.HookConfig.post;
1212
import static com.salesforce.revoman.input.config.HookConfig.pre;
13-
import static com.salesforce.revoman.input.config.KickDef.plus;
13+
import static com.salesforce.revoman.input.config.KickDef.intoMap;
1414
import static com.salesforce.revoman.input.config.ResponseConfig.unmarshallResponse;
1515
import static com.salesforce.revoman.input.config.StepPick.PostTxnStepPick.afterStepContainingHeader;
1616
import static com.salesforce.revoman.input.config.StepPick.PostTxnStepPick.afterStepContainingURIPathOfAny;
@@ -59,9 +59,9 @@ class PokemonTest {
5959
@Test
6060
void pokemon() {
6161
final var newLimit = 1;
62-
final var dynamicEnvironment1 = Map.of("offset", String.valueOf(OFFSET));
62+
final var dynamicEnvironment1 = Map.of("offset", OFFSET);
6363
final var dynamicEnvironment2 =
64-
MapsKt.mapOf(new Pair<>("limit", String.valueOf(LIMIT)), new Pair<>("null", null));
64+
MapsKt.mapOf(new Pair<>("limit", LIMIT), new Pair<>("null", null));
6565
@SuppressWarnings("Convert2Lambda")
6666
final var preLogHook =
6767
Mockito.spy(
@@ -93,7 +93,7 @@ public void accept(
9393
@NotNull Step ignore1,
9494
@NotNull TxnInfo<Request> ignore2,
9595
@NotNull Rundown rundown) {
96-
rundown.mutableEnv.set("limit", String.valueOf(newLimit));
96+
rundown.mutableEnv.set("limit", newLimit);
9797
}
9898
});
9999
@SuppressWarnings("Convert2Lambda")
@@ -102,7 +102,7 @@ public void accept(
102102
new PostStepHook() {
103103
@Override
104104
public void accept(@NotNull StepReport stepReport, @NotNull Rundown rundown) {
105-
assertThat(rundown.mutableEnv).containsEntry("limit", String.valueOf(newLimit));
105+
assertThat(rundown.mutableEnv).containsEntry("limit", newLimit);
106106
final var results =
107107
stepReport.responseInfo.get().<AllPokemon>getTypedTxnObj().results;
108108
assertThat(results.size()).isEqualTo(newLimit);
@@ -143,7 +143,7 @@ public void accept(@NotNull StepReport stepReport, @NotNull Rundown rundown) {
143143
.off();
144144
final var pokeRundown =
145145
ReVoman.revUp(
146-
config.overrideDynamicEnvironment(plus(dynamicEnvironment1, dynamicEnvironment2)));
146+
config.overrideDynamicEnvironment(intoMap(dynamicEnvironment1, dynamicEnvironment2)));
147147

148148
final var postHookFailure = pokeRundown.firstUnIgnoredUnsuccessfulStepReport().failure;
149149
assertThat(postHookFailure).containsLeftInstanceOf(PostStepHookFailure.class);
@@ -152,8 +152,8 @@ public void accept(@NotNull StepReport stepReport, @NotNull Rundown rundown) {
152152
assertThat(pokeRundown.mutableEnv)
153153
.containsExactlyEntriesIn(
154154
MapsKt.mapOf(
155-
new Pair<>("offset", String.valueOf(OFFSET)),
156-
new Pair<>("limit", String.valueOf(newLimit)),
155+
new Pair<>("offset", OFFSET),
156+
new Pair<>("limit", newLimit),
157157
new Pair<>("baseUrl", "https://pokeapi.co/api/v2"),
158158
new Pair<>("id", "1"),
159159
new Pair<>("pokemonName", "bulbasaur"),

src/main/kotlin/com/salesforce/revoman/internal/postman/PostmanSDK.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ class PostmanSDK(
5151
@Suppress("unused") fun xml2Json(xml: String): Any?
5252
}
5353

54-
internal fun getAsString(key: String): String = moshiReVoman.anyToString(environment[key])
55-
5654
inner class JSEvaluator(nodeModulesPath: String? = null) {
5755
private val jsContext: Context
5856
private var imports = ""

src/main/kotlin/com/salesforce/revoman/internal/postman/RegexReplacer.kt

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,25 @@ class RegexReplacer(
3030
*/
3131
internal fun replaceVariablesRecursively(stringWithRegex: String?, pm: PostmanSDK): String? =
3232
stringWithRegex?.let {
33-
postManVariableRegex.replace(it) { matchResult ->
34-
val variableKey = matchResult.groups[VARIABLE_KEY]?.value!!
33+
postManVariableRegex.replace(it) { variable ->
34+
val variableKey = variable.groups[VARIABLE_KEY]?.value!!
3535
customDynamicVariableGenerators[variableKey]
3636
?.let { cdvg ->
3737
replaceVariablesRecursively(
3838
cdvg.generate(variableKey, pm.currentStepReport, pm.rundown),
3939
pm,
4040
)
4141
}
42-
?.also { value -> pm.environment[variableKey] = value }
42+
?.also { value -> setItBackInEnvironment(variableKey, value, pm) }
4343
?: replaceVariablesRecursively(dynamicVariableGenerator(variableKey, pm), pm)?.also {
4444
value ->
45-
pm.environment[variableKey] = value
45+
setItBackInEnvironment(variableKey, value, pm)
4646
}
47-
?: replaceVariablesRecursively(pm.getAsString(variableKey), pm)?.also { value ->
48-
pm.environment[variableKey] = value
47+
?: replaceVariablesRecursively(pm.environment.getAsString(variableKey), pm)?.also { value
48+
->
49+
setItBackInEnvironment(variableKey, value, pm)
4950
}
50-
?: matchResult.value
51+
?: variable.value
5152
}
5253
}
5354

@@ -89,4 +90,22 @@ class RegexReplacer(
8990
else it.value
9091
},
9192
)
93+
94+
companion object {
95+
private fun setItBackInEnvironment(variableKey: String, value: String, pm: PostmanSDK) {
96+
val currentValue = pm.environment[variableKey]
97+
// * NOTE 20 Dec 2025 gopala.akshintala: Not doing `fromJson` for perf reasons.
98+
// One can always use `getTypedObj()` to deserialize
99+
val convertedValue: Any? =
100+
when (currentValue) {
101+
is Int -> value.toIntOrNull()
102+
is Long -> value.toLongOrNull()
103+
is Double -> value.toDoubleOrNull()
104+
is Float -> value.toFloatOrNull()
105+
is Boolean -> value.toBooleanStrictOrNull()
106+
else -> value
107+
}
108+
pm.environment[variableKey] = convertedValue ?: value
109+
}
110+
}
92111
}

src/main/kotlin/com/salesforce/revoman/output/postman/PostmanEnvironment.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,6 @@ constructor(
4848
logger.info { "pm environment variable unset through JS in Step: $currentStep - key: $key" }
4949
}
5050

51-
// ! TODO 24/06/23 gopala.akshintala: Support for Regex while querying environment
52-
53-
fun getAsString(key: String?) = moshiReVoman.anyToString(mutableEnv[key])
54-
55-
fun getInt(key: String?) = mutableEnv[key] as Int?
56-
5751
// ! TODO 13/09/23 gopala.akshintala: Refactor code to remove duplication
5852

5953
fun <T> mutableEnvCopyWithValuesOfType(type: Class<T>): PostmanEnvironment<T> =
@@ -112,6 +106,12 @@ constructor(
112106
moshiReVoman,
113107
)
114108

109+
// ! TODO 24/06/23 gopala.akshintala: Support for Regex while querying environment
110+
111+
fun getAsString(key: String?) = moshiReVoman.anyToString(mutableEnv[key])
112+
113+
fun getInt(key: String?) = mutableEnv[key] as Int?
114+
115115
@JvmOverloads
116116
fun <PojoT : Any> getTypedObj(
117117
key: String,

src/test/kotlin/com/salesforce/revoman/input/EvalJsTest.kt

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class EvalJsTest {
6060
pm
6161
.evaluateJS(
6262
$$"""
63-
pm.variables.replaceIn("Today is {{$currentDate}}")
63+
pm.variables.replaceIn("Today is {{$currentDate}}")
6464
"""
6565
.trimIndent()
6666
)
@@ -71,8 +71,8 @@ class EvalJsTest {
7171
fun `eval JS with moment`() {
7272
pm.evaluateJS(
7373
$$"""
74-
var moment = require('moment')
75-
pm.environment.set("$currentDate", moment().format(("YYYY-MM-DD")))
74+
var moment = require('moment')
75+
pm.environment.set("$currentDate", moment().format(("YYYY-MM-DD")))
7676
"""
7777
.trimIndent()
7878
)
@@ -83,7 +83,7 @@ class EvalJsTest {
8383
fun `eval JS with lodash`() {
8484
pm.evaluateJS(
8585
$$"""
86-
pm.environment.set("$randomNum", _.random(10))
86+
pm.environment.set("$randomNum", _.random(10))
8787
"""
8888
.trimIndent()
8989
)
@@ -95,30 +95,30 @@ class EvalJsTest {
9595
// language=xml
9696
val xmlResponse =
9797
"""
98-
<?xml version="1.0" encoding="UTF-8"?>
99-
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns="urn:partner.soap.sforce.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
100-
<soapenv:Body>
101-
<loginResponse>
102-
<result>
103-
<metadataServerUrl>https://trialorgsforu-ec.test1.my.pc-rnd.salesforce.com/services/Soap/m/55.0/00DRN0000009wGZ</metadataServerUrl>
104-
<passwordExpired>false</passwordExpired>
105-
<sandbox>false</sandbox>
106-
<serverUrl>https://trialorgsforu-ec.test1.my.pc-rnd.salesforce.com/services/Soap/u/55.0/00DRN0000009wGZ</serverUrl>
107-
<sessionId>session-key-set-in-js</sessionId>
108-
<userId>005RN000000bTH9YAM</userId>
109-
</result>
110-
</loginResponse>
111-
</soapenv:Body>
112-
</soapenv:Envelope>
98+
<?xml version="1.0" encoding="UTF-8"?>
99+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns="urn:partner.soap.sforce.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
100+
<soapenv:Body>
101+
<loginResponse>
102+
<result>
103+
<metadataServerUrl>https://trialorgsforu-ec.test1.my.pc-rnd.salesforce.com/services/Soap/m/55.0/00DRN0000009wGZ</metadataServerUrl>
104+
<passwordExpired>false</passwordExpired>
105+
<sandbox>false</sandbox>
106+
<serverUrl>https://trialorgsforu-ec.test1.my.pc-rnd.salesforce.com/services/Soap/u/55.0/00DRN0000009wGZ</serverUrl>
107+
<sessionId>session-key-set-in-js</sessionId>
108+
<userId>005RN000000bTH9YAM</userId>
109+
</result>
110+
</loginResponse>
111+
</soapenv:Body>
112+
</soapenv:Envelope>
113113
"""
114114
.trimIndent()
115115
// language=javascript
116116
val callingScript =
117117
"""
118-
var jsonData = xml2Json(responseBody);
119-
console.log(jsonData);
120-
var sessionId = jsonData['soapenv:Envelope']['soapenv:Body'].loginResponse.result.sessionId
121-
pm.environment.set("accessToken", sessionId);
118+
var jsonData = xml2Json(responseBody);
119+
console.log(jsonData);
120+
var sessionId = jsonData['soapenv:Envelope']['soapenv:Body'].loginResponse.result.sessionId
121+
pm.environment.set("accessToken", sessionId);
122122
"""
123123
.trimIndent()
124124
pm.evaluateJS(callingScript, mapOf("responseBody" to xmlResponse))
@@ -129,15 +129,15 @@ class EvalJsTest {
129129
fun `pm response to json()`() {
130130
val testScript =
131131
"""
132-
var jsonData = pm.response.json()
133-
var quoteResult = jsonData.compositeResponse[0].body.records[0]
134-
pm.environment.set("lineItemCount", quoteResult.LineItemCount)
135-
pm.environment.set("quoteCalculationStatus", quoteResult.CalculationStatus)
136-
var qlis = jsonData.compositeResponse[1].body.records
137-
qlis.forEach((record, index) => {
138-
pm.environment.set("qliCreated" + (index + 1) + "Id", record.Id)
139-
pm.environment.set("productForQLI" + (index + 1) + "Id", record.Product2Id)
140-
})
132+
var jsonData = pm.response.json()
133+
var quoteResult = jsonData.compositeResponse[0].body.records[0]
134+
pm.environment.set("lineItemCount", quoteResult.LineItemCount)
135+
pm.environment.set("quoteCalculationStatus", quoteResult.CalculationStatus)
136+
var qlis = jsonData.compositeResponse[1].body.records
137+
qlis.forEach((record, index) => {
138+
pm.environment.set("qliCreated" + (index + 1) + "Id", record.Id)
139+
pm.environment.set("productForQLI" + (index + 1) + "Id", record.Product2Id)
140+
})
141141
"""
142142
.trimIndent()
143143
val httpResponseStr = readFileToString("composite/query/resp/query-response-all-success.json")

0 commit comments

Comments
 (0)