onsi / gomega

Ginkgo's Preferred Matcher Library
http://onsi.github.io/gomega/
MIT License
2.16k stars 281 forks source link

HaveJSONPath matcher proposal #637

Open matt-simons opened 1 year ago

matt-simons commented 1 year ago

Hi, I originally raised this PR https://github.com/kubernetes-sigs/controller-runtime/pull/2174 which added a new matcher to a custom matcher library for Kubernetes however it was suggested that it might have wider benefits in the Gomega project itself. Just checking to see if there's any interest here.

The HaveJSONPath matcher works similarly to the HaveField matcher however it has the added benefit of JSON path expressions. This enables a simplified method of traversing complex structs and matching against specific fields.

For example, where k.Object is a helper function that polls an API and updates the contents of deployment:

id := func(element interface{}) string {
    return string(element.(appsv1.DeploymentCondition).Type)
}
Eventually(k.Object(deployment)).Should(HaveField("Status.Conditions",
    MatchElements(id, IgnoreExtras, Elements{
        "Available": MatchFields(IgnoreExtras, Fields{
            "Reason": Equal("MinimumReplicasAvailable"),
        }),
    }),
))

With HaveJSONPath:

Eventually(k.Object(deployment)).Should(HaveJSONPath(
    `{.status.conditions[?(@.type=="Available")].reason}`, Equal("MinimumReplicasAvailable")),
)

example JSON of deployment when marshalled:

{
...
    "status": {
        "conditions": [
            {
                "lastTransitionTime": "2023-01-24T15:40:30Z",
                "lastUpdateTime": "2023-01-24T15:40:39Z",
                "message": "ReplicaSet \"coredns-565d847f94\" has successfully progressed.",
                "reason": "NewReplicaSetAvailable",
                "status": "True",
                "type": "Progressing"
            },
            {
                "lastTransitionTime": "2023-02-03T13:48:06Z",
                "lastUpdateTime": "2023-02-03T13:48:06Z",
                "message": "Deployment has minimum availability.",
                "reason": "MinimumReplicasAvailable",
                "status": "True",
                "type": "Available"
            }
        ],
    }
}

HaveJSONPath also supports returning structs or slices for assertions and should work with any struct.

JoelSpeed commented 1 year ago

Mentioned on the other thread, a third way of writing this would be

Eventually(k.Object(deployment)).Should(HaveField("Status.Conditions",
    ContainElement(SatisfyAll(
        HaveField("Type", Equal("Available")), // Find the condition of type Available
        HaveField("Reason", Equal("MinimumReplicasAvailable")), // Check the Reason
    )),
))

which may be preferred over the previous example with MatchElements

onsi commented 1 year ago

hey there - @JoelSpeed 's correct that there are simpler ways to pull off matchers like this without needing to use MatchgElements and MatchFields.

With that said, I love the idea of adding JSONPath support. Would this traverse raw JSON objects (i.e. is the actual value passed in to the matcher a string that the matcher interprets as JSON?) or would it operate against arbitrary Go objects?

also - is there a JSONPath dependency this would rely on - and if so, what do you have in mind? or would you be implementing a parser? (both are fine, just wanting to understand). I see that the PR you linked to above uses an implementation in the k8s go-client util directory which would be an awkward dependency to pull in vs a standalone implementation of jsonpath.

lastly - it doesn't look like there's a formal JSONPath specification - and I notice that the k8s-specific JSONPath support requires enclosing the JSONPath query in { ... } - which doesn't seem to be in any of the (proto?)-specifications or various implementations I'm seeing online. If this lands in Gomega generally vs k8s specifically I think it would need to be relatively conformant to what's out there.

matt-simons commented 1 year ago

I like the idea that JSONPath could support both string and arbitrary Go objects.

The how and when I'm not too sure of, but I definitely agree it should follow the specification when formalised and I think ideally should use a well maintained library.

Maybe it would make most sense to revisit this once the specification is formalised?