11package org.evomaster.core.search.gene.builder
22
33import org.evomaster.core.search.gene.Gene
4+ import org.evomaster.core.search.gene.ObjectGene
45import org.evomaster.core.search.gene.collection.ArrayGene
56import org.evomaster.core.search.gene.collection.EnumGene
67import org.evomaster.core.search.gene.collection.PairGene
78import org.evomaster.core.search.gene.jsonpatch.JsonPatchFromPathGene
89import org.evomaster.core.search.gene.jsonpatch.JsonPatchOperationGene
910import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathOnlyGene
1011import org.evomaster.core.search.gene.jsonpatch.JsonPatchPathValueGene
12+ import org.evomaster.core.search.gene.numeric.IntegerGene
13+ import org.evomaster.core.search.gene.placeholder.CycleObjectGene
1114import org.evomaster.core.search.gene.string.StringGene
1215import org.evomaster.core.search.gene.wrapper.ChoiceGene
16+ import org.evomaster.core.search.gene.wrapper.OptionalGene
17+ import org.evomaster.core.search.service.Randomness
1318
1419/* *
1520 * Builds a JSON Patch document gene (RFC 6902).
1621 *
17- * [resourceSchema] is accepted but not yet analysed; schema-aware path extraction
18- * will be wired in a future step. Until then, [DEFAULT_PATHS] are used as placeholders.
22+ * When [resourceSchema] is provided, paths and value gene types are derived from the schema.
23+ * Otherwise, random path strings are generated using [randomness] (or a fresh [Randomness]
24+ * if none is supplied), and both [StringGene] and [IntegerGene] are offered as value choices.
1925 */
2026object JsonPatchDocumentGeneBuilder {
2127
2228 const val MIN_SIZE = 1
2329 const val DEFAULT_MAX_SIZE = 10
30+ private const val RANDOM_PATH_COUNT = 4
2431
25- val DEFAULT_PATHS = listOf (" /" , " /a" , " /b" , " /c" )
32+ /* * A single patchable field extracted from a resource schema. */
33+ internal data class SchemaField (val path : String , val gene : Gene )
34+
35+ /* *
36+ * Walks [schema] recursively and returns one [SchemaField] per reachable leaf field,
37+ * using JSON Pointer notation for paths (e.g. "/name", "/address/street").
38+ *
39+ * - [ObjectGene]: descends into each fixed field.
40+ * - [OptionalGene]: unwraps and descends using the same path prefix.
41+ * - [CycleObjectGene]: skipped to avoid infinite loops.
42+ * - Everything else (leaf genes and [ArrayGene]): treated as a patchable target at [prefix].
43+ */
44+ internal fun extractSchemaFields (schema : Gene , prefix : String = ""): List <SchemaField > {
45+ return when (schema) {
46+ is CycleObjectGene -> emptyList()
47+ is OptionalGene -> extractSchemaFields(schema.gene, prefix)
48+ is ObjectGene -> schema.fixedFields.flatMap { field ->
49+ extractSchemaFields(field, " $prefix /${field.name} " )
50+ }
51+ else -> if (prefix.isNotEmpty()) listOf (SchemaField (prefix, schema.copy())) else emptyList()
52+ }
53+ }
2654
2755 /* *
2856 * Builds the ArrayGene of patch operations.
2957 *
3058 * All six RFC 6902 operation choices (remove, move, copy, add, replace, test) are always
31- * present in the ChoiceGene template so that mutation can switch freely between them.
32- * [resourceSchema] is reserved for future schema-based path extraction and is ignored for now.
59+ * present in the ChoiceGene template.
60+ *
61+ * - If [resourceSchema] yields fields: paths and typed value genes come from the schema.
62+ * - Otherwise: paths are generated randomly via [randomness] (a fresh [Randomness] is used
63+ * when none is supplied), and value choices are [StringGene] + [IntegerGene].
3364 */
3465 fun buildOperationsArray (
35- // TODO: resourceSchema is ignored in this PR — path extraction will be wired in a follow-up
3666 resourceSchema : Gene ? = null,
37- paths : List < String > = DEFAULT_PATHS
67+ randomness : Randomness ? = null
3868 ): ArrayGene <ChoiceGene <JsonPatchOperationGene >> {
3969
40- val effectivePaths = paths.ifEmpty { listOf (" /" ) }
41- val pathEnum = EnumGene <String >(" path" , effectivePaths)
70+ val schemaFields = resourceSchema?.let { extractSchemaFields(it) }.orEmpty()
4271
43- val choices = mutableListOf<JsonPatchOperationGene >()
72+ return if (schemaFields.isNotEmpty()) {
73+ buildFromSchemaFields(schemaFields)
74+ } else {
75+ buildFromPaths(generateRandomPaths(randomness ? : Randomness ()))
76+ }
77+ }
4478
45- choices.add(JsonPatchPathOnlyGene (JsonPatchOperationGene .OP_REMOVE , JsonPatchOperationGene .OP_REMOVE , pathEnum.copy() as EnumGene <String >))
79+ // ---------------------------------------------------------------------------
80+ // private helpers
81+ // ---------------------------------------------------------------------------
82+
83+ private fun generateRandomPaths (randomness : Randomness ): List <String > =
84+ generateSequence { " /${randomness.nextWordString(2 , 6 )} " }
85+ .take(RANDOM_PATH_COUNT * 2 )
86+ .distinct()
87+ .take(RANDOM_PATH_COUNT )
88+ .toList()
89+ .ifEmpty { listOf (" /field" ) }
90+
91+ private fun buildFromSchemaFields (
92+ fields : List <SchemaField >
93+ ): ArrayGene <ChoiceGene <JsonPatchOperationGene >> {
94+ val allPaths = fields.map { it.path }
95+ val pathEnum = EnumGene <String >(" path" , allPaths)
96+
97+ val pathValueEntries: List <PairGene <EnumGene <String >, Gene >> =
98+ fields.groupBy { it.gene::class }
99+ .entries
100+ .mapIndexed { index, (_, group) ->
101+ PairGene (
102+ " entry_$index " ,
103+ EnumGene (" path" , group.map { it.path }),
104+ group.first().gene.copy()
105+ )
106+ }
107+
108+ return assemble(pathEnum, pathValueEntries)
109+ }
110+
111+ /* * No-schema case: both [StringGene] and [IntegerGene] offered as value choices. */
112+ private fun buildFromPaths (
113+ paths : List <String >
114+ ): ArrayGene <ChoiceGene <JsonPatchOperationGene >> {
115+ val pathEnum = EnumGene <String >(" path" , paths)
116+ val entries = listOf<PairGene <EnumGene <String >, Gene >> (
117+ PairGene (" entry_string" , pathEnum.copy() as EnumGene <String >, StringGene (" value" )),
118+ PairGene (" entry_int" , pathEnum.copy() as EnumGene <String >, IntegerGene (" value" ))
119+ )
120+ return assemble(pathEnum, entries)
121+ }
122+
123+ private fun assemble (
124+ pathEnum : EnumGene <String >,
125+ pathValueEntries : List <PairGene <EnumGene <String >, Gene >>
126+ ): ArrayGene <ChoiceGene <JsonPatchOperationGene >> {
127+
128+ val choices = mutableListOf<JsonPatchOperationGene >()
46129
130+ choices.add(
131+ JsonPatchPathOnlyGene (
132+ JsonPatchOperationGene .OP_REMOVE , JsonPatchOperationGene .OP_REMOVE ,
133+ pathEnum.copy() as EnumGene <String >
134+ )
135+ )
47136 choices.add(
48137 JsonPatchFromPathGene (
49- JsonPatchOperationGene .OP_MOVE ,
50- JsonPatchOperationGene .OP_MOVE ,
138+ JsonPatchOperationGene .OP_MOVE , JsonPatchOperationGene .OP_MOVE ,
51139 fromGene = pathEnum.copy() as EnumGene <String >,
52140 pathGene = pathEnum.copy() as EnumGene <String >
53141 )
54142 )
55143 choices.add(
56144 JsonPatchFromPathGene (
57- JsonPatchOperationGene .OP_COPY ,
58- JsonPatchOperationGene .OP_COPY ,
145+ JsonPatchOperationGene .OP_COPY , JsonPatchOperationGene .OP_COPY ,
59146 fromGene = pathEnum.copy() as EnumGene <String >,
60147 pathGene = pathEnum.copy() as EnumGene <String >
61148 )
62149 )
63150
64- // TODO: replace StringGene with a schema-aware gene derived from resourceSchema
65- // (e.g. IntegerGene, BooleanGene, ObjectGene) once path extraction is wired in
66- val entryTemplate = PairGene <EnumGene <String >, Gene > (
67- " entry_0" ,
68- pathEnum.copy() as EnumGene <String >,
69- StringGene (" value" )
70- )
71-
72151 for (op in listOf (JsonPatchOperationGene .OP_ADD , JsonPatchOperationGene .OP_REPLACE , JsonPatchOperationGene .OP_TEST )) {
73152 choices.add(
74153 JsonPatchPathValueGene (
75- op,
76- op,
77- ChoiceGene (" ${op} PathValue" , listOf (entryTemplate.copy() as PairGene <EnumGene <String >, Gene > ))
154+ op, op,
155+ ChoiceGene (" ${op} PathValue" , pathValueEntries.map {
156+ it.copy() as PairGene <EnumGene <String >, Gene >
157+ })
78158 )
79159 )
80160 }
81161
82- val template = ChoiceGene < JsonPatchOperationGene > (" operation" , choices)
83- return ArrayGene ( " operations " , template, minSize = MIN_SIZE , maxSize = DEFAULT_MAX_SIZE )
162+ return ArrayGene ( " operations " , ChoiceGene (" operation" , choices),
163+ minSize = MIN_SIZE , maxSize = DEFAULT_MAX_SIZE )
84164 }
85165}
0 commit comments