Json patch, initial pr, structures only#1525
Conversation
|
Hi @suarezrominajulieta, first ask for my review and then, when I aprove the PR, I will request @arcuri82 's review. |
|
Hi @jgaleotti ! Okey, done :) |
| * Provides heuristics that extract valid JSON Pointer paths from a resource schema | ||
| * and group them by field type to construct type-safe path-value pairs. | ||
| */ | ||
| object JsonPatchGeneBuilder { |
There was a problem hiding this comment.
Why it is saying that it provides "heuristics"? This class deterministically creates an JsonPathGene from a given schema
| resourceSchema: Gene? | ||
| ): ArrayGene<ChoiceGene<JsonPatchOperationGene>> { | ||
|
|
||
| val allPaths = JsonPatchGeneBuilder.extractAllPaths(resourceSchema) |
There was a problem hiding this comment.
what happens if resourceSchema is null?
There was a problem hiding this comment.
Can/should we let it do nothing for this pr?
| val choices = mutableListOf<JsonPatchOperationGene>() | ||
|
|
||
| // Operations that only need a path | ||
| choices.add(JsonPatchPathOnlyGene("remove", pathEnum.copy() as EnumGene<String>)) |
There was a problem hiding this comment.
replace "remove" and other strings with constants
|
|
||
| // Operations that need path + value; pairs are grouped by schema field type | ||
| val pathValueEntries = JsonPatchGeneBuilder.buildPathValueEntries(allPaths) | ||
| val effectiveEntries: List<PairGene<EnumGene<String>, Gene>> = |
There was a problem hiding this comment.
what does effectiveEntries mean? Shouldn't this list be called pathValuePairGenes?
| val fromGene: EnumGene<String>, | ||
| val pathGene: EnumGene<String>, | ||
| geneName: String = "${operationName}Op" | ||
| ) : JsonPatchOperationGene(geneName, operationName, listOf(fromGene, pathGene)) { |
There was a problem hiding this comment.
check that the operation name is move or copy. Why does it add the "Op" ?
| * Provides heuristics that extract valid JSON Pointer paths from a resource schema | ||
| * and group them by field type to construct type-safe path-value pairs. | ||
| */ | ||
| object JsonPatchGeneBuilder { |
There was a problem hiding this comment.
Do not include the schema extraction heuristics in this PR
| class JsonPatchPathOnlyGene( | ||
| operationName: String, | ||
| val pathGene: EnumGene<String>, | ||
| geneName: String = "${operationName}Op" |
There was a problem hiding this comment.
why do we have operationName and geneName?
There was a problem hiding this comment.
I'm not sure what I should implement. Could you explain again what we were expecting here?
I created the operationName property, which inherits from the abstract class, to verify and distinguish between a pathfrom that is a move and a pathfrom that is a copy, and to ensure that each is printed correctly.
And then there's the geneName or name, which should be the name of the gene, which can be anything, like "Gene1".
I'm wondering if the child classes should only have operationName and not name, or if we should keep both, but make them exactly the same as those defined by the parent class in the inheritance.
There was a problem hiding this comment.
I understood why we have both. Please keep geneName (name) and operationName.
| targetFormat: OutputFormat?, | ||
| extraCheck: Boolean | ||
| ): String { | ||
| val activePair = pathValueChoice.activeGene() |
There was a problem hiding this comment.
same as previous comments
|
Hi @jgaleotti ! i added the following as previously talked:
I'd appreciate it if you could review it now, thanks! |
| * a ChoiceGene that selects among the six standard operations: | ||
| * remove, move, copy, add, replace, test. | ||
| * | ||
| * [resourceSchema] is stored for future schema-aware path extraction but is not yet analysed. |
| // --- XML serialization --- | ||
|
|
||
| @Test | ||
| fun testRemoveOperationXmlFormat() { |
There was a problem hiding this comment.
Add a test case checking JSON Format for this
| } | ||
|
|
||
| @Test | ||
| fun testMoveOperationXmlFormat() { |
There was a problem hiding this comment.
Add a test case checking JSON Format
| extraCheck: Boolean | ||
| ): String { | ||
| val inner = operationsArray.getValueAsPrintableString(previousGenes, mode, targetFormat, extraCheck) | ||
| return if (mode == GeneUtils.EscapeMode.XML) "<patch>$inner</patch>" else inner |
There was a problem hiding this comment.
Add comment regarding lack of XML convention for JSON Patch documents written in XML
| */ | ||
| class JsonPatchDocumentGene private constructor( | ||
| name: String, | ||
| val resourceSchema: Gene?, |
There was a problem hiding this comment.
it seems to me that the resourceSchema does not need to be stored
| extraCheck: Boolean | ||
| ): String = formatOperation( | ||
| mode, | ||
| OpField("from", fromGene.getValueAsRawString()), |
There was a problem hiding this comment.
shouldn't this be fromGene.getValueAsPrintableString()
| } | ||
| } | ||
|
|
||
| override fun getValueAsPrintableString( |
There was a problem hiding this comment.
getValueAsPrintableString should be defined in the superclass JsonPatchOperationGene
| /** A single field in a JSON Patch operation, with its serialized value. | ||
| * [quoted] controls whether the value is wrapped in quotes in JSON output | ||
| * (true for string fields like path/from; false for typed fields like value). */ | ||
| protected data class OpField(val name: String, val value: String, val quoted: Boolean = true) |
There was a problem hiding this comment.
I think this class is not actually needed
|
Hi, @jgaleotti i made the changes we last talked about:
I had to keep getValueAsRawString with EnumGene because getValueAsPrintableString in enums, prints with "", i.e: "/example", and it would print an xml like this "/x" instead of /x. ty! |
| */ | ||
| fun buildOperationsArray( | ||
| resourceSchema: Gene? = null, | ||
| randomness: Randomness? = null |
There was a problem hiding this comment.
I don't remember discussing the creation of random paths when no resourceSchema is provided. I am not very confortable having a "nondetermintisc" gene creation.
There was a problem hiding this comment.
ok, but what should we do in case there's no schema provided, how do we build the enum genes for the paths?
There was a problem hiding this comment.
should we make it non nulleable, and throw an exception if not provided?
There was a problem hiding this comment.
Let's try having a schema by default as you used to have (the one with /a, /b, etc).
What do you think?
| // private helpers | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| private fun generateRandomPaths(randomness: Randomness): List<String> = |
There was a problem hiding this comment.
I am not sure about this renadom path creation. I think this builder should be completely deterministic.
| private fun buildFromPaths( | ||
| paths: List<String> | ||
| ): ArrayGene<ChoiceGene<JsonPatchOperationGene>> { | ||
| val pathEnum = EnumGene<String>("path", paths) |
There was a problem hiding this comment.
use constants instead of strings such as "path", "entry_string", etc.
| targetFormat: OutputFormat?, | ||
| extraCheck: Boolean | ||
| ): String { | ||
| // path/from fields use getValueAsRawString() instead of getValueAsPrintableString() because |
There was a problem hiding this comment.
I am still not sure about using getValueAsRawString(). As an example, what happens if I have an ObjectGene with an EnumGene?
|
|
||
| private fun children(randomness: Randomness = rand): List<JsonPatchOperationGene> = | ||
| JsonPatchDocumentGeneBuilder.buildOperationsArray(randomness = randomness) | ||
| .template.getViewOfChildren().map { it as JsonPatchOperationGene } |
There was a problem hiding this comment.
I do not like builder using randomness.
| val paths = (removeOp.pathGene as EnumGene<*>).values.map { it.toString() } | ||
| assertTrue(paths.isNotEmpty() && paths.all { it.startsWith("/") }) | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
Add test cases for the scenario where there are multiple paths with the same value type
| // wrong quotes inside XML tags (<path>"/x"</path>). getValueAsRawString() returns the | ||
| // bare string (e.g. /x), letting us control quoting per format ourselves. | ||
| val fields = when (this) { | ||
| is JsonPatchPathOnlyGene -> { |
There was a problem hiding this comment.
I think it is possible to have this implementation without referring explicitly to each subclass using the ``children'' fields
| // EnumGene<String>.getValueAsPrintableString() always wraps the value in quotes regardless of | ||
| // mode or targetFormat. Using it would produce double-quoting in JSON ("path":""/x"") and | ||
| // wrong quotes inside XML tags (<path>"/x"</path>). getValueAsRawString() returns the | ||
| // bare string (e.g. /x), letting us control quoting per format ourselves. |
There was a problem hiding this comment.
This is strange since I am not seeing that this is actually happening when having enumgenes in an objectgene
@Test
fun testObjectGeneWithEnumGene() {
val name = "com.example.MyDto"
val schema = """
"$name": {
"type": "object",
"properties": {
"path": {
"type": "string",
"enum": ["/some", "/some/path", "/some/other/path"]
}
},
"required": ["path"]
}
""".trimIndent()
val gene = RestActionBuilderV3.createGeneForDTO(name, schema, name, RestActionBuilderV3.Options())
assertTrue(gene is ObjectGene)
val objectGene = gene as ObjectGene
val field = objectGene.fields.find { it.name == "path" }
assertNotNull(field)
assertTrue(ParamUtil.getValueGene(field!!) is EnumGene<*>)
val enumGene = ParamUtil.getValueGene(field) as EnumGene<String>
assertEquals(3, enumGene.values.size)
assertTrue(enumGene.values.containsAll(listOf("/some", "/some/path", "/some/other/path")))
val json = objectGene.getValueAsPrintableString(listOf(), GeneUtils.EscapeMode.JSON, OutputFormat.JAVA_JUNIT_5)
assertTrue(json == """{"path":"/some"}""" || json == """{"path":"/some/path"}""" || json == """{"path":"/some/other/path"}""")
}
There was a problem hiding this comment.
you are rigth, this prints okey for json, adding the "" to the string printed (not double-quoting, that's wrong in my comment/possible implementation) => assertTrue(enumGene.values.containsAll(listOf("/some", "/some/path", "/some/other/path"))) ok.
but for xml, enumGene.getValueAsPrintableString prints the same as json, with the simple quoting, resulting in, for ex.: <operation><op>remove</op><path>"/some/path"</path></operation>, which is not xml correct.
val xmlPrintable = enumGene.getValueAsPrintableString(mode = GeneUtils.EscapeMode.XML)
assertEquals("\"/age\"", xmlPrintable)
that's why i used raw, so for json i add the simple quoting and in xml, they don't appear. but maybe we should fix the mode printing in enumgene?
Summary
Introduces the foundational gene structure and builder needed to model JSON Patch
New files
Gene model (
core/src/main/kotlin/.../search/gene/patch/)JsonPatchOperationGene— enum-backed gene representing a single RFC 6902 op (add,remove,replace,move,copy,test)JsonPatchPathOnlyGene— composite gene for ops that only require apathfield (remove)JsonPatchPathValueGene— composite gene for ops that requirepath+value(add,replace,test)JsonPatchFromPathGene— composite gene for ops that requirepath+from(move,copy)JsonPatchDocumentGene— top-level gene representing a full JSON Patch document (array of operation objects); handles serialization to JSON and mutation logicBuilder (
core/src/main/kotlin/.../problem/rest/builder/)JsonPatchGeneBuilder— constructs aJsonPatchDocumentGenefrom an OpenAPI schema, mappingPATCHrequest body fields to the appropriate sub-gene variantTests (
core/src/test/kotlin/...)GeneSamplerForTestsandGeneNumberOfGenesTestupdated to include new gene typesWhat's NOT included yet
RestActionBuilderV3(wiring the builder into the main action pipeline)Notes
This is a structural PR — all new classes compile and their unit tests pass, but the genes are not yet invoked during actual fuzzing runs.