From 4cf16e1aee2be4131c5462a9f5ff54ed795ff0e8 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 8 Nov 2022 07:00:23 -0800 Subject: [PATCH] Fix jsonPath functions (#6530) * Fix jsonPath functions - Add handling of / shorthand - Update tests to show new behaviour of safeJSONPathValue so that it is more usable Signed-off-by: Sebastian Malton * Defend CRDResourceDetails against bad jsonPaths Signed-off-by: Sebastian Malton * Update CRDResources for new behaviour of helper function Signed-off-by: Sebastian Malton * Update snapshots Signed-off-by: Sebastian Malton Signed-off-by: Sebastian Malton --- .../custom-resource-details.test.tsx.snap | 24 +++-------------- .../crd-resource-details.tsx | 5 ++-- .../+custom-resources/crd-resources.tsx | 4 +-- .../{jsonPath.test.tsx => jsonPath.test.ts} | 26 +++++++++++++------ src/renderer/utils/jsonPath.ts | 23 +++++++++++----- 5 files changed, 42 insertions(+), 40 deletions(-) rename src/renderer/utils/__tests__/{jsonPath.test.tsx => jsonPath.test.ts} (89%) diff --git a/src/renderer/components/+custom-resources/__tests__/__snapshots__/custom-resource-details.test.tsx.snap b/src/renderer/components/+custom-resources/__tests__/__snapshots__/custom-resource-details.test.tsx.snap index d3e0cd1d5a..f15bde7996 100644 --- a/src/renderer/components/+custom-resources/__tests__/__snapshots__/custom-resource-details.test.tsx.snap +++ b/src/renderer/components/+custom-resources/__tests__/__snapshots__/custom-resource-details.test.tsx.snap @@ -45,11 +45,7 @@ exports[` with a CRD with a boolean field should displa -
    -
  • - false -
  • -
+ false
@@ -101,11 +97,7 @@ exports[` with a CRD with a boolean field should displa -
    -
  • - true -
  • -
+ true
@@ -157,11 +149,7 @@ exports[` with a CRD with a number field should display -
    -
  • - 0 -
  • -
+ 0
@@ -213,11 +201,7 @@ exports[` with a CRD with a number field should display -
    -
  • - 1234 -
  • -
+ 1234
diff --git a/src/renderer/components/+custom-resources/crd-resource-details.tsx b/src/renderer/components/+custom-resources/crd-resource-details.tsx index a52d60c7ee..3bd1e821fa 100644 --- a/src/renderer/components/+custom-resources/crd-resource-details.tsx +++ b/src/renderer/components/+custom-resources/crd-resource-details.tsx @@ -15,11 +15,10 @@ import { KubeObjectMeta } from "../kube-object-meta"; import { Input } from "../input"; import type { AdditionalPrinterColumnsV1 } from "../../../common/k8s-api/endpoints/custom-resource-definition.api"; import { CustomResourceDefinition } from "../../../common/k8s-api/endpoints/custom-resource-definition.api"; -import { convertKubectlJsonPathToNodeJsonPath } from "../../utils/jsonPath"; +import { safeJSONPathValue } from "../../utils/jsonPath"; import type { KubeObjectMetadata, KubeObjectStatus } from "../../../common/k8s-api/kube-object"; import { KubeObject } from "../../../common/k8s-api/kube-object"; import logger from "../../../common/logger"; -import { JSONPath } from "@astronautlabs/jsonpath"; export interface CustomResourceDetailsProps extends KubeObjectDetailsProps { crd: CustomResourceDefinition; @@ -66,7 +65,7 @@ export class CustomResourceDetails extends React.Component ( - {convertSpecValue(JSONPath.query(resource, convertKubectlJsonPathToNodeJsonPath(jsonPath)))} + {convertSpecValue(safeJSONPathValue(resource, jsonPath))} )); } diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index 7b3d87529f..ca8884bcd3 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -11,7 +11,7 @@ import { KubeObjectListLayout } from "../kube-object-list-layout"; import type { IComputedValue } from "mobx"; import { computed, makeObservable } from "mobx"; import type { ApiManager } from "../../../common/k8s-api/api-manager"; -import { safeJSONPathValue } from "../../utils/jsonPath"; +import { formatJSONValue, safeJSONPathValue } from "../../utils/jsonPath"; import { TabLayout } from "../layout/tab-layout-2"; import { withInjectables } from "@ogre-tools/injectable-react"; import customResourcesRouteParametersInjectable from "./custom-resources-route-parameters.injectable"; @@ -73,7 +73,7 @@ class NonInjectedCustomResources extends React.Component { [columnId.age]: customResource => -customResource.getCreationTimestamp(), ...Object.fromEntries(extraColumns.map(({ name, jsonPath }) => [ name, - customResource => safeJSONPathValue(customResource, jsonPath), + customResource => formatJSONValue(safeJSONPathValue(customResource, jsonPath)), ])), }} searchFilters={[ diff --git a/src/renderer/utils/__tests__/jsonPath.test.tsx b/src/renderer/utils/__tests__/jsonPath.test.ts similarity index 89% rename from src/renderer/utils/__tests__/jsonPath.test.tsx rename to src/renderer/utils/__tests__/jsonPath.test.ts index 49e75585f9..312522a63f 100644 --- a/src/renderer/utils/__tests__/jsonPath.test.tsx +++ b/src/renderer/utils/__tests__/jsonPath.test.ts @@ -5,7 +5,7 @@ import { convertKubectlJsonPathToNodeJsonPath, safeJSONPathValue } from "../jsonPath"; -describe("parseJsonPath", () => { +describe("convertKubectlJsonPathToNodeJsonPath", () => { it("should convert \\. to use indexed notation", () => { const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels.kubesphere\\.io/alias-name"); @@ -82,13 +82,13 @@ describe("safeJSONPathValue", () => { it("should convert boolean values to strings", () => { const res = safeJSONPathValue({ bar: false }, ".bar"); - expect(res).toBe("false"); + expect(res).toBe(false); }); it("should convert number values to strings", () => { const res = safeJSONPathValue({ bar: 0 }, ".bar"); - expect(res).toBe("0"); + expect(res).toBe(0); }); it("should join sliced entries with commas only", () => { @@ -103,7 +103,7 @@ describe("safeJSONPathValue", () => { ], }, ".bar[].foo"); - expect(res).toBe("1"); + expect(res).toBe(1); }); it("should join an array of values using JSON.stringify", () => { @@ -114,7 +114,7 @@ describe("safeJSONPathValue", () => { ], }, ".bar"); - expect(res).toBe(`["world","hello"]`); + expect(res).toEqual(["world", "hello"]); }); it("should stringify an object value", () => { @@ -122,7 +122,7 @@ describe("safeJSONPathValue", () => { foo: { bar: "bat" }, }, ".foo"); - expect(res).toBe(`{"bar":"bat"}`); + expect(res).toEqual({ "bar":"bat" }); }); it("should use convertKubectlJsonPathToNodeJsonPath", () => { @@ -155,7 +155,7 @@ describe("safeJSONPathValue", () => { const res = safeJSONPathValue(obj, ".spec.metrics[*].external.highWatermark.."); - expect(res).toBe("100, 100"); + expect(res).toEqual(["100", "100"]); }); it("should not throw if path is invalid jsonpath", () => { @@ -163,6 +163,16 @@ describe("safeJSONPathValue", () => { foo: { "hello.world": "bat" }, }, "asd["); - expect(res).toBe(""); + expect(res).toBe(undefined); + }); + + it("should retrive value with '/' in jsonpath", () => { + const res = safeJSONPathValue({ + foo: { + "hello/world": "bat", + }, + }, ".foo.hello/world"); + + expect(res).toBe("bat"); }); }); diff --git a/src/renderer/utils/jsonPath.ts b/src/renderer/utils/jsonPath.ts index 3ba970a044..57fffbaea9 100644 --- a/src/renderer/utils/jsonPath.ts +++ b/src/renderer/utils/jsonPath.ts @@ -6,7 +6,7 @@ import { JSONPath } from "@astronautlabs/jsonpath"; import { TypedRegEx } from "typed-regex"; -const slashDashSearch = /[\\-]/g; +const slashDashSearch = /[/\\-]/g; const pathByBareDots = /(?<=\w)\./; const textBeforeFirstSquare = /^.*(?=\[)/g; const backSlash = /\\/g; @@ -22,6 +22,7 @@ const trailingDotDot = /\.\.$/; * * Known shorthands: * - Leading `$` is optional (but implied) + * - The string `/` can be used without a leading `\` escapement * - The string `\.` is used to denote the "value of '.'" and not "next key" * - The string `-` can be used while not in quotes * - `[]` as shorthand for `[0]` @@ -75,7 +76,15 @@ function convertToIndexNotation(key: string, firstItem = false) { } } -function formatJSONValue(value: unknown) { +export function formatJSONValue(value: unknown): string { + if (value == null) { + return ""; + } + + if (Array.isArray(value)) { + return value.map(formatJSONValue).join(", "); + } + if (typeof value === "object") { return JSON.stringify(value); } @@ -88,21 +97,21 @@ function formatJSONValue(value: unknown) { * * This function will also stringify the value retreived from the object */ -export function safeJSONPathValue(obj: object, path: string): string { +export function safeJSONPathValue(obj: object, path: string): unknown { try { const parsedPath = JSONPath.parse(convertKubectlJsonPathToNodeJsonPath(path)); - const isSlice = parsedPath.some((exp: any) => exp.expression.type === "slice" || "wildcard"); + const isSlice = parsedPath.some((exp: any) => exp.expression.type === "slice" || exp.expression.type === "wildcard"); const value = JSONPath.query(obj, JSONPath.stringify(parsedPath), isSlice ? Infinity : 1); if (isSlice) { - return value.map(formatJSONValue).join(", "); + return value; } - return formatJSONValue(value[0]); + return value[0]; } catch (error) { // something failed console.warn("[JSON-PATH]: failed to parse jsonpath", error); - return ""; + return undefined; } }