Building on the post from @rboy - I am working on serializing Jenkins build results as JSON and I keep running into stack overflow errors since the build result has a cyclic reference.
In the build results, there are references to previousBuild
and nextBuild
. If Job 2 has a previousBuild
pointing to Job 1, and Job 1 has a nextBuild
pointing to Job 2, then the entire object cannot be serialized since there is a cyclic reference.
To avoid this, I wanted a way that I could remove/replace any instances of the build result classes from my object.
Modifying the other post, I came up with the following:
import org.jenkinsci.plugins.workflow.support.steps.build.RunWrapper
// helpers
isClosure = {it instanceof Closure}
thunkify = {it -> {_ -> it}}
thunkifyValue = {isClosure(it) ? it : thunkify(it)}
makePredicate = {isClosure(it) ? it : it.&equals}
isAnyType = {types, value -> types.any{it.isInstance(value)}}
isMapOrList = isAnyType.curry([List, Map])
def cleanse(root, _predicate, _transform) {
def predicate = makePredicate(_predicate)
def transform = thunkifyValue(_transform)
if (root instanceof List) {
return root.collect {
if (isMapOrList(it)) {
it = cleanse(it, predicate, transform)
} else if (predicate(it)) {
it = transform(it)
}
return it
}
} else if (root instanceof Map) {
return root.collectEntries {k,v ->
if (isMapOrList(v)) {
v = cleanse(v, predicate, transform)
} else if (predicate(v)) {
v = transform(v)
}
return [(k): v]
}
} else {
return root
}
}
// basic usage - pass raw values
// jobs is an array of Jenkins job results
// replaces any occurrence of the value null with the value 'replaced null with string'
cleanse(jobs, null, 'replaced null with string')
// advanced usage - pass closures
// We will replace any value that is an instance of RunWrapper
// with the calculated value "Replaced Build Result - $it.inspect()"
cleanse(jobs, {it instanceof RunWrapper}, {"Replaced Build Result - ${it.inspect()}"})
After calling this on my jenkins results I am able to serialize them as JSON without getting a StackOverflowError
from the cyclic dependencies. This is how I use the cleanse
function in my code:
// testJobs contains all of the results from each `build()` in my pipeline
cleansed = cleanse(testJobs.collect {
def props = it.properties
// Something about the formatting of the values in rawBuild will cause StackOverflowError when creating json
props['rawBuild'] = props['rawBuild'].toString()
props
}, {it instanceof RunWrapper}, 'Replaced Build Result')
cleansed = cleanse(cleansed, {it instanceof Class}, 'Class stub')
// `cleansed` now has no values that are instances of RunWrapper and no values that are classes
// both of those will cause issues during JSON serialization
// render JSON
cleansedJson = groovy.json.JsonOutput.toJson(cleansed)