Skip to content

Commit

Permalink
Added JSONPath syntactic validation (#561)
Browse files Browse the repository at this point in the history
  • Loading branch information
derekpierre committed Aug 2, 2024
2 parents f7c1514 + e2d162e commit d59110a
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 43 deletions.
1 change: 1 addition & 0 deletions packages/taco/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"typedoc": "typedoc"
},
"dependencies": {
"@astronautlabs/jsonpath": "^1.1.2",
"@nucypher/nucypher-core": "*",
"@nucypher/shared": "workspace:*",
"@nucypher/taco-auth": "workspace:*",
Expand Down
23 changes: 18 additions & 5 deletions packages/taco/src/conditions/base/json-api.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
import { JSONPath } from '@astronautlabs/jsonpath';
import { z } from 'zod';

import { Condition } from '../condition';
import {
OmitConditionType,
returnValueTestSchema,
} from '../shared';
import { OmitConditionType, returnValueTestSchema } from '../shared';

export const JsonApiConditionType = 'json-api';

const validateJSONPath = (jsonPath: string): boolean => {
try {
JSONPath.parse(jsonPath);
return true;
} catch (error) {
return false;
}
};

export const jsonPathSchema = z
.string()
.refine((val) => validateJSONPath(val), {
message: 'Invalid JSONPath expression',
});

export const JsonApiConditionSchema = z.object({
conditionType: z.literal(JsonApiConditionType).default(JsonApiConditionType),
endpoint: z.string().url(),
parameters: z.record(z.string(), z.unknown()).optional(),
query: z.string().optional(),
query: jsonPathSchema.optional(),
returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods
});

Expand Down
44 changes: 44 additions & 0 deletions packages/taco/test/conditions/base/json-api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';

import { jsonPathSchema } from '../../../src/conditions/base/json-api';

describe('JSONPath Validation', () => {
it('Invalid JSONPath: Incomplete filter expression', () => {
const invalidPath = '$.store.book[?(@.price < ]';
const result = jsonPathSchema.safeParse(invalidPath);
expect(result.success).toBe(false);
expect(result.error!.errors[0].message).toBe('Invalid JSONPath expression');
});

it('Invalid JSONPath: Incorrect use of brackets', () => {
const invalidPath = '$[store][book]';
const result = jsonPathSchema.safeParse(invalidPath);
expect(result.success).toBe(false);
expect(result.error!.errors[0].message).toBe('Invalid JSONPath expression');
});

it('Invalid JSONPath: Unclosed wildcard asterisk', () => {
const invalidPath = '$.store.book[*';
const result = jsonPathSchema.safeParse(invalidPath);
expect(result.success).toBe(false);
expect(result.error!.errors[0].message).toBe('Invalid JSONPath expression');
});

it('Valid JSONPath expression', () => {
const validPath = '$.store.book[?(@.price < 10)]';
const result = jsonPathSchema.safeParse(validPath);
expect(result.success).toBe(true);
});

it('Valid JSONPath with correct quotes', () => {
const validPath = "$.store['book[?(@.price < ]']";
const result = jsonPathSchema.safeParse(validPath);
expect(result.success).toBe(true);
});

it('Valid JSONPath with correct wildcard', () => {
const validPath = '$.store.book[*]';
const result = jsonPathSchema.safeParse(validPath);
expect(result.success).toBe(true);
});
});
19 changes: 11 additions & 8 deletions packages/taco/test/conditions/base/json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ describe('JsonApiCondition', () => {
endpoint: 'not-a-url',
};

const result = JsonApiCondition.validate(JsonApiConditionSchema, badJsonApiObj);
const result = JsonApiCondition.validate(
JsonApiConditionSchema,
badJsonApiObj,
);

expect(result.error).toBeDefined();
expect(result.data).toBeUndefined();
Expand All @@ -35,26 +38,26 @@ describe('JsonApiCondition', () => {
},
});
});

describe('parameters', () => {
it('accepts conditions without query path', () => {
const { query, ...noQueryObj} = testJsonApiConditionObj;
const { query, ...noQueryObj } = testJsonApiConditionObj;
const result = JsonApiCondition.validate(
JsonApiConditionSchema,
noQueryObj
noQueryObj,
);

expect(result.error).toBeUndefined();
expect(result.data).toEqual(noQueryObj);
});

it('accepts conditions without parameters', () => {
const { query, ...noParamsObj} = testJsonApiConditionObj;
const { query, ...noParamsObj } = testJsonApiConditionObj;
const result = JsonApiCondition.validate(
JsonApiConditionSchema,
noParamsObj
noParamsObj,
);

expect(result.error).toBeUndefined();
expect(result.data).toEqual(noParamsObj);
});
Expand Down
11 changes: 7 additions & 4 deletions packages/taco/test/conditions/condition-expr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,17 +406,20 @@ describe('condition set', () => {

it('json api condition serialization', () => {
const conditionExpr = new ConditionExpression(jsonApiCondition);

const conditionExprJson = conditionExpr.toJson();
expect(conditionExprJson).toBeDefined();
expect(conditionExprJson).toContain('endpoint');
expect(conditionExprJson).toContain('https://_this_would_totally_fail.com');
expect(conditionExprJson).toContain(
'https://_this_would_totally_fail.com',
);
expect(conditionExprJson).toContain('parameters');
expect(conditionExprJson).toContain('query');
expect(conditionExprJson).toContain('$.ethereum.usd');
expect(conditionExprJson).toContain('returnValueTest');

const conditionExprFromJson = ConditionExpression.fromJSON(conditionExprJson);

const conditionExprFromJson =
ConditionExpression.fromJSON(conditionExprJson);
expect(conditionExprFromJson).toBeDefined();
expect(conditionExprFromJson.condition).toBeInstanceOf(JsonApiCondition);
});
Expand Down
9 changes: 3 additions & 6 deletions packages/taco/test/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,7 @@ import {
ContractConditionType,
FunctionAbiProps,
} from '../src/conditions/base/contract';
import {
JsonApiConditionProps,
JsonApiConditionType
} from '../src/conditions/base/json-api';
import { JsonApiConditionType } from '../src/conditions/base/json-api';
import {
RpcConditionProps,
RpcConditionType,
Expand Down Expand Up @@ -230,8 +227,8 @@ export const testJsonApiConditionObj = {
conditionType: JsonApiConditionType,
endpoint: 'https://_this_would_totally_fail.com',
parameters: {
'ids': 'ethereum',
'vs_currencies': 'usd',
ids: 'ethereum',
vs_currencies: 'usd',
},
query: '$.ethereum.usd',
returnValueTest: testReturnValueTest,
Expand Down
40 changes: 20 additions & 20 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d59110a

Please sign in to comment.