From 5687ca0907dee19cdda8b444601303024cbcc977 Mon Sep 17 00:00:00 2001 From: Erika Yao <71943596+erikayao93@users.noreply.github.com> Date: Fri, 30 Jun 2023 11:54:33 -0500 Subject: [PATCH 001/103] feat(logger): Support for external observability providers (#1511) * Updated formatAttributes for additional parameters and LogItem return type * Updated the unit tests to pass with new formatter * Updated Powertool named objects to Powertools * Updated tests to match new naming consistency * Updated for tests for new naming consistency * Updated formatter for new design decisions * Update Logger for ephemeral attributes * Update bringYourOwnFormatter documentation to match new formatter --------- Co-authored-by: erikayao93 --- packages/logger/src/Logger.ts | 4 +- .../src/formatter/LogFormatterInterface.ts | 29 ++ packages/logger/src/formatter/index.ts | 3 + .../logger/src/types/formats/PowertoolsLog.ts | 93 ++++ packages/logger/src/types/formats/index.ts | 1 + packages/logger/tests/unit/helpers.test.ts | 453 ++++++++++++++++++ 6 files changed, 581 insertions(+), 2 deletions(-) create mode 100644 packages/logger/src/formatter/LogFormatterInterface.ts create mode 100644 packages/logger/src/formatter/index.ts create mode 100644 packages/logger/src/types/formats/PowertoolsLog.ts create mode 100644 packages/logger/src/types/formats/index.ts create mode 100644 packages/logger/tests/unit/helpers.test.ts diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index 3a8859e447..d454d50a09 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -640,8 +640,8 @@ class Logger extends Utility implements LoggerInterface { item instanceof Error ? { error: item } : typeof item === 'string' - ? { extra: item } - : item; + ? { extra: item } + : item; additionalLogAttributes = merge(additionalLogAttributes, attributes); }); diff --git a/packages/logger/src/formatter/LogFormatterInterface.ts b/packages/logger/src/formatter/LogFormatterInterface.ts new file mode 100644 index 0000000000..0fe1dd9909 --- /dev/null +++ b/packages/logger/src/formatter/LogFormatterInterface.ts @@ -0,0 +1,29 @@ +import { LogAttributes, UnformattedAttributes } from '../types'; +import { LogItem } from '../log'; + +/** + * @interface + */ +interface LogFormatterInterface { + /** + * It formats key-value pairs of log attributes. + * + * @param {UnformattedAttributes} attributes + * @param {LogAttributes} additionalLogAttributes + * @returns {LogItem} + */ + formatAttributes( + attributes: UnformattedAttributes, + additionalLogAttributes: LogAttributes + ): LogItem; + + /** + * It formats a given Error parameter. + * + * @param {Error} error + * @returns {LogAttributes} + */ + formatError(error: Error): LogAttributes; +} + +export { LogFormatterInterface }; diff --git a/packages/logger/src/formatter/index.ts b/packages/logger/src/formatter/index.ts new file mode 100644 index 0000000000..ef5d7b16d8 --- /dev/null +++ b/packages/logger/src/formatter/index.ts @@ -0,0 +1,3 @@ +export * from './LogFormatter'; +export * from './LogFormatterInterface'; +export * from './PowertoolsLogFormatter'; diff --git a/packages/logger/src/types/formats/PowertoolsLog.ts b/packages/logger/src/types/formats/PowertoolsLog.ts new file mode 100644 index 0000000000..fa360fef59 --- /dev/null +++ b/packages/logger/src/types/formats/PowertoolsLog.ts @@ -0,0 +1,93 @@ +import type { LogAttributes, LogLevel } from '..'; + +type PowertoolsLog = LogAttributes & { + /** + * timestamp + * + * Description: Timestamp of actual log statement. + * Example: "2020-05-24 18:17:33,774" + */ + timestamp?: string; + + /** + * level + * + * Description: Logging level + * Example: "INFO" + */ + level?: LogLevel; + + /** + * service + * + * Description: Service name defined. + * Example: "payment" + */ + service: string; + + /** + * sampling_rate + * + * Description: The value of the logging sampling rate in percentage. + * Example: 0.1 + */ + sampling_rate?: number; + + /** + * message + * + * Description: Log statement value. Unserializable JSON values will be cast to string. + * Example: "Collecting payment" + */ + message?: string; + + /** + * xray_trace_id + * + * Description: X-Ray Trace ID when Lambda function has enabled Tracing. + * Example: "1-5759e988-bd862e3fe1be46a994272793" + */ + xray_trace_id?: string; + + /** + * cold_start + * + * Description: Indicates whether the current execution experienced a cold start. + * Example: false + */ + cold_start?: boolean; + + /** + * lambda_function_name + * + * Description: The name of the Lambda function. + * Example: "example-powertools-HelloWorldFunction-1P1Z6B39FLU73" + */ + lambda_function_name?: string; + + /** + * lambda_function_memory_size + * + * Description: The memory size of the Lambda function. + * Example: 128 + */ + lambda_function_memory_size?: number; + + /** + * lambda_function_arn + * + * Description: The ARN of the Lambda function. + * Example: "arn:aws:lambda:eu-west-1:012345678910:function:example-powertools-HelloWorldFunction-1P1Z6B39FLU73" + */ + lambda_function_arn?: string; + + /** + * lambda_request_id + * + * Description: The request ID of the current invocation. + * Example: "899856cb-83d1-40d7-8611-9e78f15f32f4" + */ + lambda_request_id?: string; +}; + +export type { PowertoolsLog }; diff --git a/packages/logger/src/types/formats/index.ts b/packages/logger/src/types/formats/index.ts new file mode 100644 index 0000000000..5a828a385f --- /dev/null +++ b/packages/logger/src/types/formats/index.ts @@ -0,0 +1 @@ +export * from './PowertoolsLog'; diff --git a/packages/logger/tests/unit/helpers.test.ts b/packages/logger/tests/unit/helpers.test.ts new file mode 100644 index 0000000000..5b45837a7e --- /dev/null +++ b/packages/logger/tests/unit/helpers.test.ts @@ -0,0 +1,453 @@ +/** + * Test Logger helpers + * + * @group unit/logger/all + */ +import { Console } from 'console'; +import { + ConfigServiceInterface, + EnvironmentVariablesService, +} from '../../src/config'; +import { LogFormatter, PowertoolsLogFormatter } from '../../src/formatter'; +import { ConstructorOptions, LogLevelThresholds } from '../../src/types'; +import { createLogger, Logger } from './../../src'; + +describe('Helper: createLogger function', () => { + const ENVIRONMENT_VARIABLES = process.env; + const logLevelThresholds: LogLevelThresholds = { + DEBUG: 8, + INFO: 12, + WARN: 16, + ERROR: 20, + CRITICAL: 24, + SILENT: 28, + }; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...ENVIRONMENT_VARIABLES }; + }); + + afterAll(() => { + process.env = ENVIRONMENT_VARIABLES; + }); + + describe('LoggerOptions constructor parameters', () => { + test('when no constructor parameters are set, returns a Logger instance with the options set in the environment variables', () => { + // Prepare + const loggerOptions = undefined; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'hello-world', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + defaultServiceName: 'service_undefined', + logLevel: 8, + logFormatter: expect.any(PowertoolsLogFormatter), + }) + ); + }); + + test('when no parameters are set, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + logLevel: 'WARN', + serviceName: 'my-lambda-service', + sampleRateValue: 1, + logFormatter: new PowertoolsLogFormatter(), + customConfigService: new EnvironmentVariablesService(), + persistentLogAttributes: { + awsAccountId: '123456789', + }, + environment: 'prod', + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual({ + coldStart: true, + defaultServiceName: 'service_undefined', + customConfigService: expect.any(EnvironmentVariablesService), + envVarsService: expect.any(EnvironmentVariablesService), + logEvent: false, + logIndentation: 0, + logFormatter: expect.any(PowertoolsLogFormatter), + logLevel: 16, + console: expect.any(Console), + logLevelThresholds: { + ...logLevelThresholds, + }, + logsSampled: true, + persistentLogAttributes: { + awsAccountId: '123456789', + }, + powertoolLogData: { + awsRegion: 'eu-west-1', + environment: 'prod', + sampleRateValue: 1, + serviceName: 'my-lambda-service', + }, + }); + }); + + test('when no constructor parameters and no environment variables are set, returns a Logger instance with the default properties', () => { + // Prepare + const loggerOptions = undefined; + delete process.env.POWERTOOLS_SERVICE_NAME; + delete process.env.POWERTOOLS_LOG_LEVEL; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual({ + coldStart: true, + customConfigService: undefined, + defaultServiceName: 'service_undefined', + envVarsService: expect.any(EnvironmentVariablesService), + logEvent: false, + logIndentation: 0, + logFormatter: expect.any(PowertoolsLogFormatter), + logLevel: 12, + console: expect.any(Console), + logLevelThresholds: { + ...logLevelThresholds, + }, + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + awsRegion: 'eu-west-1', + environment: '', + sampleRateValue: undefined, + serviceName: 'service_undefined', + }, + }); + }); + + test('when a custom logFormatter is passed, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + logFormatter: expect.any(LogFormatter), + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'hello-world', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + logLevel: 8, + logFormatter: expect.any(LogFormatter), + }) + ); + }); + + test('when a custom serviceName is passed, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + serviceName: 'my-backend-service', + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'my-backend-service', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + logLevel: 8, + logFormatter: {}, + }) + ); + }); + + test('when a custom uppercase logLevel is passed, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + logLevel: 'ERROR', + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'hello-world', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + logLevel: 20, + logFormatter: expect.any(PowertoolsLogFormatter), + }) + ); + }); + + test('when a custom lowercase logLevel is passed, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + logLevel: 'warn', + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'hello-world', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + logLevel: 16, + logFormatter: expect.any(PowertoolsLogFormatter), + }) + ); + }); + + test('when no log level is set, returns a Logger instance with INFO level', () => { + // Prepare + const loggerOptions: ConstructorOptions = {}; + delete process.env.POWERTOOLS_LOG_LEVEL; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual({ + coldStart: true, + customConfigService: undefined, + defaultServiceName: 'service_undefined', + envVarsService: expect.any(EnvironmentVariablesService), + logEvent: false, + logIndentation: 0, + logFormatter: expect.any(PowertoolsLogFormatter), + logLevel: 12, + console: expect.any(Console), + logLevelThresholds: { + ...logLevelThresholds, + }, + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + awsRegion: 'eu-west-1', + environment: '', + sampleRateValue: undefined, + serviceName: 'hello-world', + }, + }); + }); + + test('when a custom sampleRateValue is passed, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + sampleRateValue: 1, + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: true, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: 1, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'hello-world', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + logLevel: 8, + logFormatter: {}, + }) + ); + }); + + test('when a custom customConfigService is passed, returns a Logger instance with the correct properties', () => { + const configService: ConfigServiceInterface = { + get(name: string): string { + return `a-string-from-${name}`; + }, + getAwsLogLevel(): string { + return 'INFO'; + }, + getCurrentEnvironment(): string { + return 'dev'; + }, + getLogEvent(): boolean { + return true; + }, + getLogLevel(): string { + return 'INFO'; + }, + getSampleRateValue(): number | undefined { + return undefined; + }, + getServiceName(): string { + return 'my-backend-service'; + }, + isDevMode(): boolean { + return false; + }, + isValueTrue(): boolean { + return true; + }, + }; + // Prepare + const loggerOptions: ConstructorOptions = { + customConfigService: configService, + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: 'dev', + serviceName: 'my-backend-service', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: configService, + logLevel: 12, + logFormatter: {}, + }) + ); + }); + + test('when custom persistentLogAttributes is passed, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + persistentLogAttributes: { + aws_account_id: '123456789012', + aws_region: 'eu-west-1', + logger: { + name: 'aws-lambda-powertool-typescript', + version: '0.2.4', + }, + }, + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: { + aws_account_id: '123456789012', + aws_region: 'eu-west-1', + logger: { + name: 'aws-lambda-powertool-typescript', + version: '0.2.4', + }, + }, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: '', + serviceName: 'hello-world', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + logLevel: 8, + logFormatter: {}, + }) + ); + }); + + test('when a custom environment is passed, returns a Logger instance with the correct properties', () => { + // Prepare + const loggerOptions: ConstructorOptions = { + environment: 'dev', + }; + + // Act + const logger = createLogger(loggerOptions); + + // Assess + expect(logger).toBeInstanceOf(Logger); + expect(logger).toEqual( + expect.objectContaining({ + logsSampled: false, + persistentLogAttributes: {}, + powertoolLogData: { + sampleRateValue: undefined, + awsRegion: 'eu-west-1', + environment: 'dev', + serviceName: 'hello-world', + }, + envVarsService: expect.any(EnvironmentVariablesService), + customConfigService: undefined, + logLevel: 8, + logFormatter: {}, + }) + ); + }); + }); +}); From e4ab44ae5f2676285a4138e694ed6642cc6e8e00 Mon Sep 17 00:00:00 2001 From: Erika Yao <71943596+erikayao93@users.noreply.github.com> Date: Fri, 30 Jun 2023 11:54:33 -0500 Subject: [PATCH 002/103] feat(logger): Support for external observability providers (#1511) * Updated formatAttributes for additional parameters and LogItem return type * Updated the unit tests to pass with new formatter * Updated Powertool named objects to Powertools * Updated tests to match new naming consistency * Updated for tests for new naming consistency * Updated formatter for new design decisions * Update Logger for ephemeral attributes * Update bringYourOwnFormatter documentation to match new formatter --------- Co-authored-by: erikayao93 --- packages/logger/src/formatter/PowertoolsLogFormatter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/logger/src/formatter/PowertoolsLogFormatter.ts b/packages/logger/src/formatter/PowertoolsLogFormatter.ts index 80c0406bbb..10a87e61f6 100644 --- a/packages/logger/src/formatter/PowertoolsLogFormatter.ts +++ b/packages/logger/src/formatter/PowertoolsLogFormatter.ts @@ -16,7 +16,7 @@ class PowertoolsLogFormatter extends LogFormatter { * * @param {UnformattedAttributes} attributes * @param {LogAttributes} additionalLogAttributes - * @returns {LogItem} + * @returns {PowertoolsLog} */ public formatAttributes( attributes: UnformattedAttributes, From 9d98c860ce7f5e6c113978f26f3b467c1ceeec98 Mon Sep 17 00:00:00 2001 From: Erika Yao <71943596+erikayao93@users.noreply.github.com> Date: Fri, 7 Jul 2023 08:58:21 -0500 Subject: [PATCH 003/103] chore(logger): PowertoolsLogFormatter docstring and variable naming update (#1585) * Updated formatAttributes for additional parameters and LogItem return type * Updated the unit tests to pass with new formatter * Updated Powertool named objects to Powertools * Updated tests to match new naming consistency * Updated for tests for new naming consistency * Updated formatter for new design decisions * Update Logger for ephemeral attributes * Update bringYourOwnFormatter documentation to match new formatter * Fixed incorrect return type, renamed variable for consistency --- packages/logger/src/formatter/PowertoolsLogFormatter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/logger/src/formatter/PowertoolsLogFormatter.ts b/packages/logger/src/formatter/PowertoolsLogFormatter.ts index 10a87e61f6..80c0406bbb 100644 --- a/packages/logger/src/formatter/PowertoolsLogFormatter.ts +++ b/packages/logger/src/formatter/PowertoolsLogFormatter.ts @@ -16,7 +16,7 @@ class PowertoolsLogFormatter extends LogFormatter { * * @param {UnformattedAttributes} attributes * @param {LogAttributes} additionalLogAttributes - * @returns {PowertoolsLog} + * @returns {LogItem} */ public formatAttributes( attributes: UnformattedAttributes, From 6d1bfba9c1410fcc1b7ece5801e2411ea42b1302 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sat, 16 Sep 2023 07:06:22 +0200 Subject: [PATCH 004/103] chore(maintenance): bump dependencies & drop nodejs14x (#1687) --- .github/ISSUE_TEMPLATE/bug_report.yml | 12 ++++++------ .../reusable-run-linting-check-and-unit-tests.yml | 4 ++-- examples/cdk/package.json | 2 +- examples/sam/package.json | 2 +- layers/package.json | 2 +- lerna.json | 2 +- packages/idempotency/package.json | 2 +- packages/logger/package.json | 2 +- packages/metrics/package.json | 3 ++- packages/parameters/package.json | 2 +- packages/testing/package.json | 2 +- packages/tracer/package.json | 2 +- 12 files changed, 19 insertions(+), 18 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 06af982ec3..3b9c913987 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,14 +1,14 @@ name: Bug report description: Report a reproducible bug to help us improve -title: "Bug: TITLE" -labels: ["type/bug", "triage"] -projects: ["aws-powertools/7"] +title: 'Bug: TITLE' +labels: ['type/bug', 'triage'] +projects: ['aws-powertools/7'] body: - type: markdown attributes: value: | Thank you for submitting a bug report. Before you start, make sure that [the bug hasn't been reported already](https://github.com/aws-powertools/powertools-lambda-typescript/issues). - + Please add as much information as possible to help us reproduce, and remove any potential sensitive data. - type: textarea id: expected_behaviour @@ -58,7 +58,7 @@ body: id: version attributes: label: Powertools for AWS Lambda (TypeScript) version - placeholder: "latest, 1.3.0" + placeholder: 'latest, 2.0.0' value: latest validations: required: true @@ -95,4 +95,4 @@ body: value: | --- - **Disclaimer**: After creating an issue, please wait until it is triaged and confirmed by a maintainer before implementing it. This will reduce amount of rework and the chance that a pull request gets rejected. \ No newline at end of file + **Disclaimer**: After creating an issue, please wait until it is triaged and confirmed by a maintainer before implementing it. This will reduce amount of rework and the chance that a pull request gets rejected. diff --git a/.github/workflows/reusable-run-linting-check-and-unit-tests.yml b/.github/workflows/reusable-run-linting-check-and-unit-tests.yml index d7d7b2bd0a..4398d4a8be 100644 --- a/.github/workflows/reusable-run-linting-check-and-unit-tests.yml +++ b/.github/workflows/reusable-run-linting-check-and-unit-tests.yml @@ -22,7 +22,7 @@ jobs: uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: ${{ matrix.version }} - cache: "npm" + cache: 'npm' - name: Setup dependencies uses: ./.github/actions/cached-node-modules with: @@ -37,7 +37,7 @@ jobs: NODE_ENV: dev strategy: matrix: - example: ["sam", "cdk"] + example: ['sam', 'cdk'] fail-fast: false defaults: run: diff --git a/examples/cdk/package.json b/examples/cdk/package.json index fd29e2fe09..cbdb13d0c5 100644 --- a/examples/cdk/package.json +++ b/examples/cdk/package.json @@ -50,4 +50,4 @@ "phin": "^3.7.0", "source-map-support": "^0.5.21" } -} +} \ No newline at end of file diff --git a/examples/sam/package.json b/examples/sam/package.json index 647596ed14..499fc879c6 100644 --- a/examples/sam/package.json +++ b/examples/sam/package.json @@ -41,4 +41,4 @@ "@middy/core": "^4.7.0", "phin": "^3.7.0" } -} +} \ No newline at end of file diff --git a/layers/package.json b/layers/package.json index b2365d1ae8..6f56ec817f 100644 --- a/layers/package.json +++ b/layers/package.json @@ -43,4 +43,4 @@ "aws-cdk-lib": "^2.130.0", "esbuild": "^0.20.1" } -} +} \ No newline at end of file diff --git a/lerna.json b/lerna.json index 4ddb451f9e..9beb95c0f1 100644 --- a/lerna.json +++ b/lerna.json @@ -16,4 +16,4 @@ "version": "2.0.2", "npmClient": "npm", "message": "chore(release): %s [skip ci]" -} +} \ No newline at end of file diff --git a/packages/idempotency/package.json b/packages/idempotency/package.json index 5984549a7d..54664020f0 100644 --- a/packages/idempotency/package.json +++ b/packages/idempotency/package.json @@ -136,4 +136,4 @@ "aws-sdk-client-mock": "^3.0.1", "aws-sdk-client-mock-jest": "^3.0.1" } -} +} \ No newline at end of file diff --git a/packages/logger/package.json b/packages/logger/package.json index 7da74efebb..136f6760ed 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -100,4 +100,4 @@ "serverless", "nodejs" ] -} +} \ No newline at end of file diff --git a/packages/metrics/package.json b/packages/metrics/package.json index 477dd9c2cb..31c511cc4e 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -12,6 +12,7 @@ "scripts": { "test": "npm run test:unit", "test:unit": "jest --group=unit --detectOpenHandles --coverage --verbose", + "jest": "jest --detectOpenHandles --verbose", "test:e2e:nodejs16x": "RUNTIME=nodejs16x jest --group=e2e", "test:e2e:nodejs18x": "RUNTIME=nodejs18x jest --group=e2e", "test:e2e:nodejs20x": "RUNTIME=nodejs20x jest --group=e2e", @@ -99,4 +100,4 @@ "serverless", "nodejs" ] -} +} \ No newline at end of file diff --git a/packages/parameters/package.json b/packages/parameters/package.json index ba05a0d60b..2469a717ee 100644 --- a/packages/parameters/package.json +++ b/packages/parameters/package.json @@ -198,4 +198,4 @@ "optional": true } } -} +} \ No newline at end of file diff --git a/packages/testing/package.json b/packages/testing/package.json index 0e470554b9..66aea8a0a3 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -100,4 +100,4 @@ "aws-cdk-lib": "^2.130.0", "esbuild": "^0.20.1" } -} +} \ No newline at end of file diff --git a/packages/tracer/package.json b/packages/tracer/package.json index a88df7c2f6..1832af85c7 100644 --- a/packages/tracer/package.json +++ b/packages/tracer/package.json @@ -105,4 +105,4 @@ "serverless", "nodejs" ] -} +} \ No newline at end of file From e6279bbea57bffccdcb4516a1d88d313410042cd Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 19 Sep 2023 00:41:40 +0200 Subject: [PATCH 005/103] chore: update release script to mark all utilities as alpha --- .github/scripts/release_patch_package_json.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/scripts/release_patch_package_json.js b/.github/scripts/release_patch_package_json.js index 761c0680a1..31c091aeb8 100644 --- a/.github/scripts/release_patch_package_json.js +++ b/.github/scripts/release_patch_package_json.js @@ -17,7 +17,15 @@ if (process.argv.length < 3) { } const basePath = resolve(process.argv[2]); const packageJsonPath = join(basePath, 'package.json'); -const alphaPackages = []; +const alphaPackages = [ + '@aws-lambda-powertools/batch', + '@aws-lambda-powertools/commons', + '@aws-lambda-powertools/idempotency', + '@aws-lambda-powertools/logger', + '@aws-lambda-powertools/metrics', + '@aws-lambda-powertools/parameters', + '@aws-lambda-powertools/tracer', +]; const betaPackages = []; (() => { From dc44030b93b4112939c7343b62378bd36302f12e Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 27 Sep 2023 19:13:25 +0200 Subject: [PATCH 006/103] chore: release version change --- .github/scripts/release_patch_package_json.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/scripts/release_patch_package_json.js b/.github/scripts/release_patch_package_json.js index 31c091aeb8..761c0680a1 100644 --- a/.github/scripts/release_patch_package_json.js +++ b/.github/scripts/release_patch_package_json.js @@ -17,15 +17,7 @@ if (process.argv.length < 3) { } const basePath = resolve(process.argv[2]); const packageJsonPath = join(basePath, 'package.json'); -const alphaPackages = [ - '@aws-lambda-powertools/batch', - '@aws-lambda-powertools/commons', - '@aws-lambda-powertools/idempotency', - '@aws-lambda-powertools/logger', - '@aws-lambda-powertools/metrics', - '@aws-lambda-powertools/parameters', - '@aws-lambda-powertools/tracer', -]; +const alphaPackages = []; const betaPackages = []; (() => { From dc3f64258b8abd1b5712b5469ea89a9c55e35541 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 27 Sep 2023 19:19:08 +0200 Subject: [PATCH 007/103] chore: release version change --- .github/scripts/release_patch_package_json.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/scripts/release_patch_package_json.js b/.github/scripts/release_patch_package_json.js index 761c0680a1..31c091aeb8 100644 --- a/.github/scripts/release_patch_package_json.js +++ b/.github/scripts/release_patch_package_json.js @@ -17,7 +17,15 @@ if (process.argv.length < 3) { } const basePath = resolve(process.argv[2]); const packageJsonPath = join(basePath, 'package.json'); -const alphaPackages = []; +const alphaPackages = [ + '@aws-lambda-powertools/batch', + '@aws-lambda-powertools/commons', + '@aws-lambda-powertools/idempotency', + '@aws-lambda-powertools/logger', + '@aws-lambda-powertools/metrics', + '@aws-lambda-powertools/parameters', + '@aws-lambda-powertools/tracer', +]; const betaPackages = []; (() => { From 67e5e948660107e1c77d43e63c1a6f6c87bd4c78 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 3 Oct 2023 00:34:58 +0200 Subject: [PATCH 008/103] chore(maintenance): remove `createLogger` and `createTracer` helpers (#1722) * chore(maintenance): bump dependencies & drop nodejs14x (#1687) * chore: add pre-release script * chore: restore deps * chore: added v2 shim * chore(maintenance): remove logger and tracer helper function * chore: remove imports * chore: fix deps & versions * tests: moved unit tests * tests: move logger tests * chore: added v2 shim * chore: added v2 shim --- .github/scripts/release_patch_package_json.js | 8 +- docs/snippets/package.json | 2 +- packages/logger/tests/unit/helpers.test.ts | 453 ------------------ v2.json | 3 + 4 files changed, 11 insertions(+), 455 deletions(-) delete mode 100644 packages/logger/tests/unit/helpers.test.ts create mode 100644 v2.json diff --git a/.github/scripts/release_patch_package_json.js b/.github/scripts/release_patch_package_json.js index 31c091aeb8..c11ec86864 100644 --- a/.github/scripts/release_patch_package_json.js +++ b/.github/scripts/release_patch_package_json.js @@ -58,7 +58,10 @@ const betaPackages = []; let version = originalVersion; // If the package is an alpha or beta package, update the version number to include a suffix if (alphaPackages.includes(name)) { - version = `${version}-alpha`; + const iteration = JSON.parse( + readFileSync(resolve('..', '..', 'v2.json'), 'utf8') + ).iteration; + version = `${version}-alpha.${iteration}`; } else if (betaPackages.includes(name)) { version = `${version}-beta`; } @@ -81,6 +84,9 @@ const betaPackages = []; types, files, type, + scripts: { + postinstall: `echo 'WARNING: This is a pre-release version of Powertools for AWS (TypeScript) provided for evaluation only. Do not use in production.'`, + }, }; // Not all utilities have these fields, so only add them if they exist to avoid diff --git a/docs/snippets/package.json b/docs/snippets/package.json index e42e3ddcbb..351867b442 100644 --- a/docs/snippets/package.json +++ b/docs/snippets/package.json @@ -40,4 +40,4 @@ "axios": "^1.6.7", "hashi-vault-js": "^0.4.14" } -} +} \ No newline at end of file diff --git a/packages/logger/tests/unit/helpers.test.ts b/packages/logger/tests/unit/helpers.test.ts deleted file mode 100644 index 5b45837a7e..0000000000 --- a/packages/logger/tests/unit/helpers.test.ts +++ /dev/null @@ -1,453 +0,0 @@ -/** - * Test Logger helpers - * - * @group unit/logger/all - */ -import { Console } from 'console'; -import { - ConfigServiceInterface, - EnvironmentVariablesService, -} from '../../src/config'; -import { LogFormatter, PowertoolsLogFormatter } from '../../src/formatter'; -import { ConstructorOptions, LogLevelThresholds } from '../../src/types'; -import { createLogger, Logger } from './../../src'; - -describe('Helper: createLogger function', () => { - const ENVIRONMENT_VARIABLES = process.env; - const logLevelThresholds: LogLevelThresholds = { - DEBUG: 8, - INFO: 12, - WARN: 16, - ERROR: 20, - CRITICAL: 24, - SILENT: 28, - }; - - beforeEach(() => { - jest.resetModules(); - process.env = { ...ENVIRONMENT_VARIABLES }; - }); - - afterAll(() => { - process.env = ENVIRONMENT_VARIABLES; - }); - - describe('LoggerOptions constructor parameters', () => { - test('when no constructor parameters are set, returns a Logger instance with the options set in the environment variables', () => { - // Prepare - const loggerOptions = undefined; - - // Act - const logger = createLogger(loggerOptions); - - // Assess - expect(logger).toBeInstanceOf(Logger); - expect(logger).toEqual( - expect.objectContaining({ - logsSampled: false, - persistentLogAttributes: {}, - powertoolLogData: { - sampleRateValue: undefined, - awsRegion: 'eu-west-1', - environment: '', - serviceName: 'hello-world', - }, - envVarsService: expect.any(EnvironmentVariablesService), - customConfigService: undefined, - defaultServiceName: 'service_undefined', - logLevel: 8, - logFormatter: expect.any(PowertoolsLogFormatter), - }) - ); - }); - - test('when no parameters are set, returns a Logger instance with the correct properties', () => { - // Prepare - const loggerOptions: ConstructorOptions = { - logLevel: 'WARN', - serviceName: 'my-lambda-service', - sampleRateValue: 1, - logFormatter: new PowertoolsLogFormatter(), - customConfigService: new EnvironmentVariablesService(), - persistentLogAttributes: { - awsAccountId: '123456789', - }, - environment: 'prod', - }; - - // Act - const logger = createLogger(loggerOptions); - - // Assess - expect(logger).toBeInstanceOf(Logger); - expect(logger).toEqual({ - coldStart: true, - defaultServiceName: 'service_undefined', - customConfigService: expect.any(EnvironmentVariablesService), - envVarsService: expect.any(EnvironmentVariablesService), - logEvent: false, - logIndentation: 0, - logFormatter: expect.any(PowertoolsLogFormatter), - logLevel: 16, - console: expect.any(Console), - logLevelThresholds: { - ...logLevelThresholds, - }, - logsSampled: true, - persistentLogAttributes: { - awsAccountId: '123456789', - }, - powertoolLogData: { - awsRegion: 'eu-west-1', - environment: 'prod', - sampleRateValue: 1, - serviceName: 'my-lambda-service', - }, - }); - }); - - test('when no constructor parameters and no environment variables are set, returns a Logger instance with the default properties', () => { - // Prepare - const loggerOptions = undefined; - delete process.env.POWERTOOLS_SERVICE_NAME; - delete process.env.POWERTOOLS_LOG_LEVEL; - - // Act - const logger = createLogger(loggerOptions); - - // Assess - expect(logger).toBeInstanceOf(Logger); - expect(logger).toEqual({ - coldStart: true, - customConfigService: undefined, - defaultServiceName: 'service_undefined', - envVarsService: expect.any(EnvironmentVariablesService), - logEvent: false, - logIndentation: 0, - logFormatter: expect.any(PowertoolsLogFormatter), - logLevel: 12, - console: expect.any(Console), - logLevelThresholds: { - ...logLevelThresholds, - }, - logsSampled: false, - persistentLogAttributes: {}, - powertoolLogData: { - awsRegion: 'eu-west-1', - environment: '', - sampleRateValue: undefined, - serviceName: 'service_undefined', - }, - }); - }); - - test('when a custom logFormatter is passed, returns a Logger instance with the correct properties', () => { - // Prepare - const loggerOptions: ConstructorOptions = { - logFormatter: expect.any(LogFormatter), - }; - - // Act - const logger = createLogger(loggerOptions); - - // Assess - expect(logger).toBeInstanceOf(Logger); - expect(logger).toEqual( - expect.objectContaining({ - logsSampled: false, - persistentLogAttributes: {}, - powertoolLogData: { - sampleRateValue: undefined, - awsRegion: 'eu-west-1', - environment: '', - serviceName: 'hello-world', - }, - envVarsService: expect.any(EnvironmentVariablesService), - customConfigService: undefined, - logLevel: 8, - logFormatter: expect.any(LogFormatter), - }) - ); - }); - - test('when a custom serviceName is passed, returns a Logger instance with the correct properties', () => { - // Prepare - const loggerOptions: ConstructorOptions = { - serviceName: 'my-backend-service', - }; - - // Act - const logger = createLogger(loggerOptions); - - // Assess - expect(logger).toBeInstanceOf(Logger); - expect(logger).toEqual( - expect.objectContaining({ - logsSampled: false, - persistentLogAttributes: {}, - powertoolLogData: { - sampleRateValue: undefined, - awsRegion: 'eu-west-1', - environment: '', - serviceName: 'my-backend-service', - }, - envVarsService: expect.any(EnvironmentVariablesService), - customConfigService: undefined, - logLevel: 8, - logFormatter: {}, - }) - ); - }); - - test('when a custom uppercase logLevel is passed, returns a Logger instance with the correct properties', () => { - // Prepare - const loggerOptions: ConstructorOptions = { - logLevel: 'ERROR', - }; - - // Act - const logger = createLogger(loggerOptions); - - // Assess - expect(logger).toBeInstanceOf(Logger); - expect(logger).toEqual( - expect.objectContaining({ - logsSampled: false, - persistentLogAttributes: {}, - powertoolLogData: { - sampleRateValue: undefined, - awsRegion: 'eu-west-1', - environment: '', - serviceName: 'hello-world', - }, - envVarsService: expect.any(EnvironmentVariablesService), - customConfigService: undefined, - logLevel: 20, - logFormatter: expect.any(PowertoolsLogFormatter), - }) - ); - }); - - test('when a custom lowercase logLevel is passed, returns a Logger instance with the correct properties', () => { - // Prepare - const loggerOptions: ConstructorOptions = { - logLevel: 'warn', - }; - - // Act - const logger = createLogger(loggerOptions); - - // Assess - expect(logger).toBeInstanceOf(Logger); - expect(logger).toEqual( - expect.objectContaining({ - logsSampled: false, - persistentLogAttributes: {}, - powertoolLogData: { - sampleRateValue: undefined, - awsRegion: 'eu-west-1', - environment: '', - serviceName: 'hello-world', - }, - envVarsService: expect.any(EnvironmentVariablesService), - customConfigService: undefined, - logLevel: 16, - logFormatter: expect.any(PowertoolsLogFormatter), - }) - ); - }); - - test('when no log level is set, returns a Logger instance with INFO level', () => { - // Prepare - const loggerOptions: ConstructorOptions = {}; - delete process.env.POWERTOOLS_LOG_LEVEL; - - // Act - const logger = createLogger(loggerOptions); - - // Assess - expect(logger).toBeInstanceOf(Logger); - expect(logger).toEqual({ - coldStart: true, - customConfigService: undefined, - defaultServiceName: 'service_undefined', - envVarsService: expect.any(EnvironmentVariablesService), - logEvent: false, - logIndentation: 0, - logFormatter: expect.any(PowertoolsLogFormatter), - logLevel: 12, - console: expect.any(Console), - logLevelThresholds: { - ...logLevelThresholds, - }, - logsSampled: false, - persistentLogAttributes: {}, - powertoolLogData: { - awsRegion: 'eu-west-1', - environment: '', - sampleRateValue: undefined, - serviceName: 'hello-world', - }, - }); - }); - - test('when a custom sampleRateValue is passed, returns a Logger instance with the correct properties', () => { - // Prepare - const loggerOptions: ConstructorOptions = { - sampleRateValue: 1, - }; - - // Act - const logger = createLogger(loggerOptions); - - // Assess - expect(logger).toBeInstanceOf(Logger); - expect(logger).toEqual( - expect.objectContaining({ - logsSampled: true, - persistentLogAttributes: {}, - powertoolLogData: { - sampleRateValue: 1, - awsRegion: 'eu-west-1', - environment: '', - serviceName: 'hello-world', - }, - envVarsService: expect.any(EnvironmentVariablesService), - customConfigService: undefined, - logLevel: 8, - logFormatter: {}, - }) - ); - }); - - test('when a custom customConfigService is passed, returns a Logger instance with the correct properties', () => { - const configService: ConfigServiceInterface = { - get(name: string): string { - return `a-string-from-${name}`; - }, - getAwsLogLevel(): string { - return 'INFO'; - }, - getCurrentEnvironment(): string { - return 'dev'; - }, - getLogEvent(): boolean { - return true; - }, - getLogLevel(): string { - return 'INFO'; - }, - getSampleRateValue(): number | undefined { - return undefined; - }, - getServiceName(): string { - return 'my-backend-service'; - }, - isDevMode(): boolean { - return false; - }, - isValueTrue(): boolean { - return true; - }, - }; - // Prepare - const loggerOptions: ConstructorOptions = { - customConfigService: configService, - }; - - // Act - const logger = createLogger(loggerOptions); - - // Assess - expect(logger).toBeInstanceOf(Logger); - expect(logger).toEqual( - expect.objectContaining({ - logsSampled: false, - persistentLogAttributes: {}, - powertoolLogData: { - sampleRateValue: undefined, - awsRegion: 'eu-west-1', - environment: 'dev', - serviceName: 'my-backend-service', - }, - envVarsService: expect.any(EnvironmentVariablesService), - customConfigService: configService, - logLevel: 12, - logFormatter: {}, - }) - ); - }); - - test('when custom persistentLogAttributes is passed, returns a Logger instance with the correct properties', () => { - // Prepare - const loggerOptions: ConstructorOptions = { - persistentLogAttributes: { - aws_account_id: '123456789012', - aws_region: 'eu-west-1', - logger: { - name: 'aws-lambda-powertool-typescript', - version: '0.2.4', - }, - }, - }; - - // Act - const logger = createLogger(loggerOptions); - - // Assess - expect(logger).toBeInstanceOf(Logger); - expect(logger).toEqual( - expect.objectContaining({ - logsSampled: false, - persistentLogAttributes: { - aws_account_id: '123456789012', - aws_region: 'eu-west-1', - logger: { - name: 'aws-lambda-powertool-typescript', - version: '0.2.4', - }, - }, - powertoolLogData: { - sampleRateValue: undefined, - awsRegion: 'eu-west-1', - environment: '', - serviceName: 'hello-world', - }, - envVarsService: expect.any(EnvironmentVariablesService), - customConfigService: undefined, - logLevel: 8, - logFormatter: {}, - }) - ); - }); - - test('when a custom environment is passed, returns a Logger instance with the correct properties', () => { - // Prepare - const loggerOptions: ConstructorOptions = { - environment: 'dev', - }; - - // Act - const logger = createLogger(loggerOptions); - - // Assess - expect(logger).toBeInstanceOf(Logger); - expect(logger).toEqual( - expect.objectContaining({ - logsSampled: false, - persistentLogAttributes: {}, - powertoolLogData: { - sampleRateValue: undefined, - awsRegion: 'eu-west-1', - environment: 'dev', - serviceName: 'hello-world', - }, - envVarsService: expect.any(EnvironmentVariablesService), - customConfigService: undefined, - logLevel: 8, - logFormatter: {}, - }) - ); - }); - }); -}); diff --git a/v2.json b/v2.json new file mode 100644 index 0000000000..f732091fe5 --- /dev/null +++ b/v2.json @@ -0,0 +1,3 @@ +{ + "iteration": 0 +} \ No newline at end of file From 9e3a811c5ebadd7ae1777a03f32561cc4bc2af73 Mon Sep 17 00:00:00 2001 From: Ant Stanley Date: Thu, 12 Oct 2023 00:18:04 +0200 Subject: [PATCH 009/103] feat(logger): add esmodule support (#1734) * feat(logger): add esm build output * fix(Logger): Remove barrel files update references * test(Logger): update jest/ts-jest to use ESM * chore(Logger): remove unused lodash.merge * fix(logger): reinstate lodash.merge * chore(logger): revert TS assertion * chore(logger): revert format changes * chore(logger): update postbuild to remove incremental tsbuildinfo files * fix(logger): correct reference to types output * feat(logging): add middleware export * chore(logger): replace postbuild script with echo statement * feat(logger): add typesVersions property and barrel files to /types * chore(logger): file not used, can be added back if needed * chore(logger): add space back to README * chore(logger): revert space in README --- packages/logger/jest.config.cjs | 7 ++++++- packages/logger/package.json | 2 +- packages/logger/src/formatter/LogFormatterInterface.ts | 5 +++-- packages/logger/src/formatter/index.ts | 3 --- packages/logger/src/log/LogItemInterface.ts | 9 +++++++++ packages/logger/src/types/formats/PowertoolsLog.ts | 2 +- packages/logger/src/types/formats/index.ts | 2 +- 7 files changed, 21 insertions(+), 9 deletions(-) delete mode 100644 packages/logger/src/formatter/index.ts create mode 100644 packages/logger/src/log/LogItemInterface.ts diff --git a/packages/logger/jest.config.cjs b/packages/logger/jest.config.cjs index deb2a416a5..f711f79a2c 100644 --- a/packages/logger/jest.config.cjs +++ b/packages/logger/jest.config.cjs @@ -8,7 +8,12 @@ module.exports = { '^(\\.{1,2}/.*)\\.js$': '$1', }, transform: { - '^.+\\.ts?$': 'ts-jest', + '^.+\\.[tj]sx?$': [ + 'ts-jest', + { + useESM: true, + }, + ], }, moduleFileExtensions: ['js', 'ts'], collectCoverageFrom: ['**/src/**/*.ts', '!**/node_modules/**'], diff --git a/packages/logger/package.json b/packages/logger/package.json index 136f6760ed..7da74efebb 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -100,4 +100,4 @@ "serverless", "nodejs" ] -} \ No newline at end of file +} diff --git a/packages/logger/src/formatter/LogFormatterInterface.ts b/packages/logger/src/formatter/LogFormatterInterface.ts index 0fe1dd9909..49d35154c3 100644 --- a/packages/logger/src/formatter/LogFormatterInterface.ts +++ b/packages/logger/src/formatter/LogFormatterInterface.ts @@ -1,5 +1,6 @@ -import { LogAttributes, UnformattedAttributes } from '../types'; -import { LogItem } from '../log'; +import { LogAttributes } from '../types/Log.js'; +import { UnformattedAttributes } from '../types/Logger.js'; +import { LogItem } from '../log/LogItem.js'; /** * @interface diff --git a/packages/logger/src/formatter/index.ts b/packages/logger/src/formatter/index.ts deleted file mode 100644 index ef5d7b16d8..0000000000 --- a/packages/logger/src/formatter/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './LogFormatter'; -export * from './LogFormatterInterface'; -export * from './PowertoolsLogFormatter'; diff --git a/packages/logger/src/log/LogItemInterface.ts b/packages/logger/src/log/LogItemInterface.ts new file mode 100644 index 0000000000..ed0c89bb0b --- /dev/null +++ b/packages/logger/src/log/LogItemInterface.ts @@ -0,0 +1,9 @@ +import { LogAttributes } from '../types/Log.js'; + +interface LogItemInterface { + addAttributes(attributes: LogAttributes): void; + + getAttributes(): LogAttributes; +} + +export { LogItemInterface }; diff --git a/packages/logger/src/types/formats/PowertoolsLog.ts b/packages/logger/src/types/formats/PowertoolsLog.ts index fa360fef59..9406a2a5fe 100644 --- a/packages/logger/src/types/formats/PowertoolsLog.ts +++ b/packages/logger/src/types/formats/PowertoolsLog.ts @@ -1,4 +1,4 @@ -import type { LogAttributes, LogLevel } from '..'; +import type { LogAttributes, LogLevel } from '../Log.js'; type PowertoolsLog = LogAttributes & { /** diff --git a/packages/logger/src/types/formats/index.ts b/packages/logger/src/types/formats/index.ts index 5a828a385f..03dabd2013 100644 --- a/packages/logger/src/types/formats/index.ts +++ b/packages/logger/src/types/formats/index.ts @@ -1 +1 @@ -export * from './PowertoolsLog'; +export type { PowertoolsLog } from './PowertoolsLog.js'; From 74842c8586baa6883565c452bc8e115f8698720e Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 12 Oct 2023 01:46:25 +0200 Subject: [PATCH 010/103] feat(commons): add esmodule support (#1735) * chore(logger): adapt logger to commons exports * feat(commons): add esmodule support * chore: address sonar findings * chore(commons): exported version * chore: fixed imports in examples * chore(parameters): fixed imports * chore(metrics): fixed imports * chore(tracer): fixed imports * chore(idempotency): fixed imports * chore(commons): test coverage * chore(batch): fix imports --- packages/batch/tests/unit/processPartialResponse.test.ts | 8 ++++---- .../batch/tests/unit/processPartialResponseSync.test.ts | 8 ++++---- packages/commons/src/samples/resources/contexts/index.ts | 1 + packages/commons/src/samples/resources/events/index.ts | 1 + packages/logger/jest.config.cjs | 7 +------ packages/logger/package.json | 2 +- 6 files changed, 12 insertions(+), 15 deletions(-) create mode 100644 packages/commons/src/samples/resources/contexts/index.ts create mode 100644 packages/commons/src/samples/resources/events/index.ts diff --git a/packages/batch/tests/unit/processPartialResponse.test.ts b/packages/batch/tests/unit/processPartialResponse.test.ts index 732d40ee3b..a224be9990 100644 --- a/packages/batch/tests/unit/processPartialResponse.test.ts +++ b/packages/batch/tests/unit/processPartialResponse.test.ts @@ -110,7 +110,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = await handler(event, context); + const result = await handler(event, context.helloworldContext); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); @@ -137,7 +137,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = await handler(event, context); + const result = await handler(event, context.helloworldContext); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); @@ -164,7 +164,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = await handler(event, context); + const result = await handler(event, context.helloworldContext); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); @@ -223,7 +223,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = await handler(event, context); + const result = await handler(event, context.helloworldContext); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); diff --git a/packages/batch/tests/unit/processPartialResponseSync.test.ts b/packages/batch/tests/unit/processPartialResponseSync.test.ts index 4fe7da8609..7bab3ba846 100644 --- a/packages/batch/tests/unit/processPartialResponseSync.test.ts +++ b/packages/batch/tests/unit/processPartialResponseSync.test.ts @@ -110,7 +110,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = handler(event, context); + const result = handler(event, context.helloworldContext); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); @@ -137,7 +137,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = handler(event, context); + const result = handler(event, context.helloworldContext); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); @@ -164,7 +164,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = handler(event, context); + const result = handler(event, context.helloworldContext); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); @@ -219,7 +219,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = handler(event, context); + const result = handler(event, context.helloworldContext); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); diff --git a/packages/commons/src/samples/resources/contexts/index.ts b/packages/commons/src/samples/resources/contexts/index.ts new file mode 100644 index 0000000000..910f356d2c --- /dev/null +++ b/packages/commons/src/samples/resources/contexts/index.ts @@ -0,0 +1 @@ +export * from './hello-world.js'; diff --git a/packages/commons/src/samples/resources/events/index.ts b/packages/commons/src/samples/resources/events/index.ts new file mode 100644 index 0000000000..94b087e514 --- /dev/null +++ b/packages/commons/src/samples/resources/events/index.ts @@ -0,0 +1 @@ +export * as Custom from './custom/index.js'; diff --git a/packages/logger/jest.config.cjs b/packages/logger/jest.config.cjs index f711f79a2c..deb2a416a5 100644 --- a/packages/logger/jest.config.cjs +++ b/packages/logger/jest.config.cjs @@ -8,12 +8,7 @@ module.exports = { '^(\\.{1,2}/.*)\\.js$': '$1', }, transform: { - '^.+\\.[tj]sx?$': [ - 'ts-jest', - { - useESM: true, - }, - ], + '^.+\\.ts?$': 'ts-jest', }, moduleFileExtensions: ['js', 'ts'], collectCoverageFrom: ['**/src/**/*.ts', '!**/node_modules/**'], diff --git a/packages/logger/package.json b/packages/logger/package.json index 7da74efebb..136f6760ed 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -100,4 +100,4 @@ "serverless", "nodejs" ] -} +} \ No newline at end of file From 54b3f27a6a47c421ed4fa52ddb9a109e80feae12 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 12 Oct 2023 12:56:40 +0200 Subject: [PATCH 011/103] feat(internal): add esmodule support (#1738) * feat(testing): add esmodule support * chore(all): update imports --- packages/batch/tests/unit/processPartialResponse.test.ts | 8 ++++---- .../batch/tests/unit/processPartialResponseSync.test.ts | 8 ++++---- packages/commons/src/samples/resources/contexts/index.ts | 1 - packages/commons/src/samples/resources/events/index.ts | 1 - 4 files changed, 8 insertions(+), 10 deletions(-) delete mode 100644 packages/commons/src/samples/resources/contexts/index.ts delete mode 100644 packages/commons/src/samples/resources/events/index.ts diff --git a/packages/batch/tests/unit/processPartialResponse.test.ts b/packages/batch/tests/unit/processPartialResponse.test.ts index a224be9990..732d40ee3b 100644 --- a/packages/batch/tests/unit/processPartialResponse.test.ts +++ b/packages/batch/tests/unit/processPartialResponse.test.ts @@ -110,7 +110,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = await handler(event, context.helloworldContext); + const result = await handler(event, context); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); @@ -137,7 +137,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = await handler(event, context.helloworldContext); + const result = await handler(event, context); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); @@ -164,7 +164,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = await handler(event, context.helloworldContext); + const result = await handler(event, context); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); @@ -223,7 +223,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = await handler(event, context.helloworldContext); + const result = await handler(event, context); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); diff --git a/packages/batch/tests/unit/processPartialResponseSync.test.ts b/packages/batch/tests/unit/processPartialResponseSync.test.ts index 7bab3ba846..4fe7da8609 100644 --- a/packages/batch/tests/unit/processPartialResponseSync.test.ts +++ b/packages/batch/tests/unit/processPartialResponseSync.test.ts @@ -110,7 +110,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = handler(event, context.helloworldContext); + const result = handler(event, context); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); @@ -137,7 +137,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = handler(event, context.helloworldContext); + const result = handler(event, context); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); @@ -164,7 +164,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = handler(event, context.helloworldContext); + const result = handler(event, context); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); @@ -219,7 +219,7 @@ describe('Function: processPartialResponse()', () => { }; // Act - const result = handler(event, context.helloworldContext); + const result = handler(event, context); // Assess expect(result).toStrictEqual({ batchItemFailures: [] }); diff --git a/packages/commons/src/samples/resources/contexts/index.ts b/packages/commons/src/samples/resources/contexts/index.ts deleted file mode 100644 index 910f356d2c..0000000000 --- a/packages/commons/src/samples/resources/contexts/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './hello-world.js'; diff --git a/packages/commons/src/samples/resources/events/index.ts b/packages/commons/src/samples/resources/events/index.ts deleted file mode 100644 index 94b087e514..0000000000 --- a/packages/commons/src/samples/resources/events/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * as Custom from './custom/index.js'; From 1f41355e365b9bcfb97d3b007c71aae2efdbf014 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sat, 14 Oct 2023 02:54:39 +0200 Subject: [PATCH 012/103] chore(ci): v2 release line --- .github/scripts/release_patch_package_json.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/scripts/release_patch_package_json.js b/.github/scripts/release_patch_package_json.js index c11ec86864..932bc7b109 100644 --- a/.github/scripts/release_patch_package_json.js +++ b/.github/scripts/release_patch_package_json.js @@ -62,6 +62,12 @@ const betaPackages = []; readFileSync(resolve('..', '..', 'v2.json'), 'utf8') ).iteration; version = `${version}-alpha.${iteration}`; + dependencies && + Object.entries(dependencies).forEach(([dependencyName, version]) => { + if (alphaPackages.includes(dependencyName)) { + dependencies[dependencyName] = `${version}-alpha.${iteration}`; + } + }); } else if (betaPackages.includes(name)) { version = `${version}-beta`; } @@ -84,9 +90,6 @@ const betaPackages = []; types, files, type, - scripts: { - postinstall: `echo 'WARNING: This is a pre-release version of Powertools for AWS (TypeScript) provided for evaluation only. Do not use in production.'`, - }, }; // Not all utilities have these fields, so only add them if they exist to avoid From 0f52ad13be167d63b7a3aefac966da175b92d39b Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sun, 15 Oct 2023 19:31:26 +0200 Subject: [PATCH 013/103] chore(ci): fix alpha versioning pre-release --- .github/workflows/make-v2-release.yml | 84 +++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 .github/workflows/make-v2-release.yml diff --git a/.github/workflows/make-v2-release.yml b/.github/workflows/make-v2-release.yml new file mode 100644 index 0000000000..c1cb957609 --- /dev/null +++ b/.github/workflows/make-v2-release.yml @@ -0,0 +1,84 @@ +name: Make Release v2 (pre-release) +on: + workflow_dispatch: {} + +permissions: + contents: read + +concurrency: + group: on-release-publish +jobs: + run-unit-tests: + uses: ./.github/workflows/reusable-run-linting-check-and-unit-tests.yml + publish-npm: + needs: run-unit-tests + # Needed as recommended by npm docs on publishing with provenance https://docs.npmjs.com/generating-provenance-statements + permissions: + id-token: write + environment: Release + runs-on: ubuntu-latest + outputs: + RELEASE_VERSION: ${{ steps.set-release-version.outputs.RELEASE_VERSION }} + steps: + - name: Checkout code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Setup NodeJS + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: + node-version: '20' + cache: 'npm' + - name: Setup auth tokens + run: | + npm set "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" + - name: Setup dependencies + uses: ./.github/actions/cached-node-modules + with: + # We don't build the packages here as we want to version them first + build: false + - name: Version + run: | + # Version all packages to next major version (2.0.0) without pushing to git, generating changelog or running commit hooks + # Since the version stored in the lerna.json will always be a 1.x.x version, we manually set the version to 2.0.0 + npx lerna version major --force-publish --no-push --no-git-tag-version --no-commit-hooks --no-changelog --yes + - name: Set alpha iteration + run: | + # Get the current alpha version from npm i.e 2.0.0-alpha.0 -> 0, 2.0.0-alpha.1 -> 1 (default to -1 if no alpha versions exist = first pre-release) + ITERATION=$(npm show @aws-lambda-powertools/commons time --json | jq -r 'to_entries | map(select(.key | startswith("2.0.0-alpha"))) | sort_by(.key) | last | .key // "-1"' | cut -d '.' -f 4) + # Write the new version to the file + echo "{ \"iteration\": $((ITERATION + 1)) }" > v2.json + - name: Increment version in UA + run: | + # Increment the version in the UA + echo -e "// this file is auto generated, do not modify\nexport const PT_VERSION = '2.0.0-alpha.$(jq -r '.iteration' v2.json)';" > packages/commons/src/version.ts + - name: Build + run: | + npm run build -w packages/commons & + npm run build -w packages/batch \ + -w packages/idempotency \ + -w packages/logger \ + -w packages/metrics \ + -w packages/parameters \ + -w packages/tracer + - name: Pack packages + run: | + npm pack -w packages/batch \ + -w packages/commons \ + -w packages/idempotency \ + -w packages/logger \ + -w packages/metrics \ + -w packages/parameters \ + -w packages/tracer + - name: Publish to npm + run: | + npm publish aws-lambda-powertools-batch-*.tgz --tag next --provenance + npm publish aws-lambda-powertools-commons-*.tgz --tag next --provenance + npm publish aws-lambda-powertools-idempotency-*.tgz --tag next --provenance + npm publish aws-lambda-powertools-logger-*.tgz --tag next --provenance + npm publish aws-lambda-powertools-metrics-*.tgz --tag next --provenance + npm publish aws-lambda-powertools-parameters-*.tgz --tag next --provenance + npm publish aws-lambda-powertools-tracer-*.tgz --tag next --provenance + - name: Set release version + id: set-release-version + run: | + VERSION="2.0.0-alpha.$(cat v2.json | jq .iteration -r)" + echo RELEASE_VERSION="$VERSION" >> "$GITHUB_OUTPUT" From 0d82499d8b46b99797aff396daa3211ddc9c6375 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 18 Oct 2023 13:43:32 +0200 Subject: [PATCH 014/103] docs(maintenance): add processes tab (#1747) * docs(maintenance): update mkdocs to support tabs * chore(ci): add parallel test npm script * chore(ci): add jest command * docs(maintenance): add testing page to navbar * docs(maintenance): add contributing info * chore: update roadmap * chore: update release drafter workflow to allow for manual trigger * fix formatting * docs: maintainers handbook * chore: link to new location * fix links * Update docs/maintainers.md Co-authored-by: Alexander Schueren --------- Co-authored-by: Alexander Schueren --- .github/workflows/make-v2-release.yml | 84 --------------------------- package.json | 2 +- packages/batch/package.json | 2 +- 3 files changed, 2 insertions(+), 86 deletions(-) delete mode 100644 .github/workflows/make-v2-release.yml diff --git a/.github/workflows/make-v2-release.yml b/.github/workflows/make-v2-release.yml deleted file mode 100644 index c1cb957609..0000000000 --- a/.github/workflows/make-v2-release.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Make Release v2 (pre-release) -on: - workflow_dispatch: {} - -permissions: - contents: read - -concurrency: - group: on-release-publish -jobs: - run-unit-tests: - uses: ./.github/workflows/reusable-run-linting-check-and-unit-tests.yml - publish-npm: - needs: run-unit-tests - # Needed as recommended by npm docs on publishing with provenance https://docs.npmjs.com/generating-provenance-statements - permissions: - id-token: write - environment: Release - runs-on: ubuntu-latest - outputs: - RELEASE_VERSION: ${{ steps.set-release-version.outputs.RELEASE_VERSION }} - steps: - - name: Checkout code - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - name: Setup NodeJS - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version: '20' - cache: 'npm' - - name: Setup auth tokens - run: | - npm set "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" - - name: Setup dependencies - uses: ./.github/actions/cached-node-modules - with: - # We don't build the packages here as we want to version them first - build: false - - name: Version - run: | - # Version all packages to next major version (2.0.0) without pushing to git, generating changelog or running commit hooks - # Since the version stored in the lerna.json will always be a 1.x.x version, we manually set the version to 2.0.0 - npx lerna version major --force-publish --no-push --no-git-tag-version --no-commit-hooks --no-changelog --yes - - name: Set alpha iteration - run: | - # Get the current alpha version from npm i.e 2.0.0-alpha.0 -> 0, 2.0.0-alpha.1 -> 1 (default to -1 if no alpha versions exist = first pre-release) - ITERATION=$(npm show @aws-lambda-powertools/commons time --json | jq -r 'to_entries | map(select(.key | startswith("2.0.0-alpha"))) | sort_by(.key) | last | .key // "-1"' | cut -d '.' -f 4) - # Write the new version to the file - echo "{ \"iteration\": $((ITERATION + 1)) }" > v2.json - - name: Increment version in UA - run: | - # Increment the version in the UA - echo -e "// this file is auto generated, do not modify\nexport const PT_VERSION = '2.0.0-alpha.$(jq -r '.iteration' v2.json)';" > packages/commons/src/version.ts - - name: Build - run: | - npm run build -w packages/commons & - npm run build -w packages/batch \ - -w packages/idempotency \ - -w packages/logger \ - -w packages/metrics \ - -w packages/parameters \ - -w packages/tracer - - name: Pack packages - run: | - npm pack -w packages/batch \ - -w packages/commons \ - -w packages/idempotency \ - -w packages/logger \ - -w packages/metrics \ - -w packages/parameters \ - -w packages/tracer - - name: Publish to npm - run: | - npm publish aws-lambda-powertools-batch-*.tgz --tag next --provenance - npm publish aws-lambda-powertools-commons-*.tgz --tag next --provenance - npm publish aws-lambda-powertools-idempotency-*.tgz --tag next --provenance - npm publish aws-lambda-powertools-logger-*.tgz --tag next --provenance - npm publish aws-lambda-powertools-metrics-*.tgz --tag next --provenance - npm publish aws-lambda-powertools-parameters-*.tgz --tag next --provenance - npm publish aws-lambda-powertools-tracer-*.tgz --tag next --provenance - - name: Set release version - id: set-release-version - run: | - VERSION="2.0.0-alpha.$(cat v2.json | jq .iteration -r)" - echo RELEASE_VERSION="$VERSION" >> "$GITHUB_OUTPUT" diff --git a/package.json b/package.json index c1549c894e..e30fa7c00f 100644 --- a/package.json +++ b/package.json @@ -74,4 +74,4 @@ "engines": { "node": ">=16" } -} +} \ No newline at end of file diff --git a/packages/batch/package.json b/packages/batch/package.json index 3495a7cf08..de694a5589 100644 --- a/packages/batch/package.json +++ b/packages/batch/package.json @@ -79,4 +79,4 @@ "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing" } -} +} \ No newline at end of file From b4853debedbac679c8fc016b14ba06c3d0411a4c Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 18 Oct 2023 20:00:21 +0200 Subject: [PATCH 015/103] chore: remove extra comma --- packages/tracer/tests/unit/Tracer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tracer/tests/unit/Tracer.test.ts b/packages/tracer/tests/unit/Tracer.test.ts index 04747e1f9f..8c4e374640 100644 --- a/packages/tracer/tests/unit/Tracer.test.ts +++ b/packages/tracer/tests/unit/Tracer.test.ts @@ -20,7 +20,7 @@ type CaptureAsyncFuncMock = jest.SpyInstance< [ name: string, fcn: (subsegment?: Subsegment) => unknown, - parent?: Segment | Subsegment, + parent?: Segment | Subsegment ] >; const createCaptureAsyncFuncMock = function ( From 9f50daa338cf16eaf8ce746e38c7fe2f6285e080 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sat, 21 Oct 2023 00:12:24 +0200 Subject: [PATCH 016/103] chore(logger): refactor types and interfaces (#1758) * chore(logger): refactor types and interfaces * chore: grouped type files * chore: fix code smell * chore: fix ci * chore: fix ci --- packages/logger/src/Logger.ts | 4 +- packages/logger/src/formatter/LogFormatter.ts | 7 +- .../src/formatter/LogFormatterInterface.ts | 30 ------ packages/logger/src/log/LogItemInterface.ts | 9 -- .../logger/src/types/formats/PowertoolsLog.ts | 93 ------------------- packages/logger/src/types/formats/index.ts | 1 - packages/tracer/tests/unit/Tracer.test.ts | 2 +- tsconfig.json | 2 +- 8 files changed, 7 insertions(+), 141 deletions(-) delete mode 100644 packages/logger/src/formatter/LogFormatterInterface.ts delete mode 100644 packages/logger/src/log/LogItemInterface.ts delete mode 100644 packages/logger/src/types/formats/PowertoolsLog.ts delete mode 100644 packages/logger/src/types/formats/index.ts diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index d454d50a09..810c1a8613 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -432,8 +432,8 @@ class Logger extends Utility implements LoggerInterface { logger.addContext(context); let shouldLogEvent = undefined; - if (options && options.hasOwnProperty('logEvent')) { - shouldLogEvent = options.logEvent; + if (Object.hasOwn(options || {}, 'logEvent')) { + shouldLogEvent = options!.logEvent; } logger.logEventIfEnabled(event, shouldLogEvent); } diff --git a/packages/logger/src/formatter/LogFormatter.ts b/packages/logger/src/formatter/LogFormatter.ts index dd874732c4..48016377a7 100644 --- a/packages/logger/src/formatter/LogFormatter.ts +++ b/packages/logger/src/formatter/LogFormatter.ts @@ -65,11 +65,10 @@ abstract class LogFormatter implements LogFormatterInterface { location: this.getCodeLocation(error.stack), message: error.message, stack: error.stack, - cause: isErrorWithCause(error) - ? error.cause instanceof Error + cause: + error.cause instanceof Error ? this.formatError(error.cause) - : error.cause - : undefined, + : error.cause, }; } diff --git a/packages/logger/src/formatter/LogFormatterInterface.ts b/packages/logger/src/formatter/LogFormatterInterface.ts deleted file mode 100644 index 49d35154c3..0000000000 --- a/packages/logger/src/formatter/LogFormatterInterface.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { LogAttributes } from '../types/Log.js'; -import { UnformattedAttributes } from '../types/Logger.js'; -import { LogItem } from '../log/LogItem.js'; - -/** - * @interface - */ -interface LogFormatterInterface { - /** - * It formats key-value pairs of log attributes. - * - * @param {UnformattedAttributes} attributes - * @param {LogAttributes} additionalLogAttributes - * @returns {LogItem} - */ - formatAttributes( - attributes: UnformattedAttributes, - additionalLogAttributes: LogAttributes - ): LogItem; - - /** - * It formats a given Error parameter. - * - * @param {Error} error - * @returns {LogAttributes} - */ - formatError(error: Error): LogAttributes; -} - -export { LogFormatterInterface }; diff --git a/packages/logger/src/log/LogItemInterface.ts b/packages/logger/src/log/LogItemInterface.ts deleted file mode 100644 index ed0c89bb0b..0000000000 --- a/packages/logger/src/log/LogItemInterface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { LogAttributes } from '../types/Log.js'; - -interface LogItemInterface { - addAttributes(attributes: LogAttributes): void; - - getAttributes(): LogAttributes; -} - -export { LogItemInterface }; diff --git a/packages/logger/src/types/formats/PowertoolsLog.ts b/packages/logger/src/types/formats/PowertoolsLog.ts deleted file mode 100644 index 9406a2a5fe..0000000000 --- a/packages/logger/src/types/formats/PowertoolsLog.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { LogAttributes, LogLevel } from '../Log.js'; - -type PowertoolsLog = LogAttributes & { - /** - * timestamp - * - * Description: Timestamp of actual log statement. - * Example: "2020-05-24 18:17:33,774" - */ - timestamp?: string; - - /** - * level - * - * Description: Logging level - * Example: "INFO" - */ - level?: LogLevel; - - /** - * service - * - * Description: Service name defined. - * Example: "payment" - */ - service: string; - - /** - * sampling_rate - * - * Description: The value of the logging sampling rate in percentage. - * Example: 0.1 - */ - sampling_rate?: number; - - /** - * message - * - * Description: Log statement value. Unserializable JSON values will be cast to string. - * Example: "Collecting payment" - */ - message?: string; - - /** - * xray_trace_id - * - * Description: X-Ray Trace ID when Lambda function has enabled Tracing. - * Example: "1-5759e988-bd862e3fe1be46a994272793" - */ - xray_trace_id?: string; - - /** - * cold_start - * - * Description: Indicates whether the current execution experienced a cold start. - * Example: false - */ - cold_start?: boolean; - - /** - * lambda_function_name - * - * Description: The name of the Lambda function. - * Example: "example-powertools-HelloWorldFunction-1P1Z6B39FLU73" - */ - lambda_function_name?: string; - - /** - * lambda_function_memory_size - * - * Description: The memory size of the Lambda function. - * Example: 128 - */ - lambda_function_memory_size?: number; - - /** - * lambda_function_arn - * - * Description: The ARN of the Lambda function. - * Example: "arn:aws:lambda:eu-west-1:012345678910:function:example-powertools-HelloWorldFunction-1P1Z6B39FLU73" - */ - lambda_function_arn?: string; - - /** - * lambda_request_id - * - * Description: The request ID of the current invocation. - * Example: "899856cb-83d1-40d7-8611-9e78f15f32f4" - */ - lambda_request_id?: string; -}; - -export type { PowertoolsLog }; diff --git a/packages/logger/src/types/formats/index.ts b/packages/logger/src/types/formats/index.ts deleted file mode 100644 index 03dabd2013..0000000000 --- a/packages/logger/src/types/formats/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { PowertoolsLog } from './PowertoolsLog.js'; diff --git a/packages/tracer/tests/unit/Tracer.test.ts b/packages/tracer/tests/unit/Tracer.test.ts index 8c4e374640..04747e1f9f 100644 --- a/packages/tracer/tests/unit/Tracer.test.ts +++ b/packages/tracer/tests/unit/Tracer.test.ts @@ -20,7 +20,7 @@ type CaptureAsyncFuncMock = jest.SpyInstance< [ name: string, fcn: (subsegment?: Subsegment) => unknown, - parent?: Segment | Subsegment + parent?: Segment | Subsegment, ] >; const createCaptureAsyncFuncMock = function ( diff --git a/tsconfig.json b/tsconfig.json index 06b00be287..08d5af9848 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "incremental": true, "composite": true, - "target": "ES2021", // Node.js 16 + "target": "ES2022", // Node.js 16 "experimentalDecorators": true, "module": "commonjs", "moduleResolution": "node", From b08e589752118981acc0104ca38991d383d37fd4 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sat, 21 Oct 2023 00:48:29 +0200 Subject: [PATCH 017/103] chore(maintenance): bump Middy v4 & run tests (#1760) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e30fa7c00f..c1549c894e 100644 --- a/package.json +++ b/package.json @@ -74,4 +74,4 @@ "engines": { "node": ">=16" } -} \ No newline at end of file +} From cf8750f9d4a0d18e049382001d9876d5069a1abf Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sat, 21 Oct 2023 12:25:27 +0200 Subject: [PATCH 018/103] chore(layers) widen version check in e2e --- layers/tests/e2e/layerPublisher.class.test.functionCode.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/layers/tests/e2e/layerPublisher.class.test.functionCode.ts b/layers/tests/e2e/layerPublisher.class.test.functionCode.ts index b95eeafea9..3f15dfbbb1 100644 --- a/layers/tests/e2e/layerPublisher.class.test.functionCode.ts +++ b/layers/tests/e2e/layerPublisher.class.test.functionCode.ts @@ -91,9 +91,12 @@ export const handler = async (): Promise => { 'batch', ]) { const moduleVersion = await getVersionFromModule(moduleName); - if (moduleVersion != expectedVersion) { + // TODO: remove this check once v2 becomes GA + // if (moduleVersion != expectedVersion) { + if (!moduleVersion.startsWith(expectedVersion)) { throw new Error( - `Package version mismatch (${moduleName}): ${moduleVersion} != ${expectedVersion}` + // `Package version mismatch (${moduleName}): ${moduleVersion} != ${expectedVersion}` + `Package version mismatch (${moduleName}): ${moduleVersion} does not start with ${expectedVersion}` ); } } From 60c816f846ba4c1cf6c2078e1b75526dd56c4339 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 23 Oct 2023 20:34:09 +0200 Subject: [PATCH 019/103] chore(maintenance): enable `isolatedModules` and isolate cache (#1765) * chore(layers) widen version check in e2e * chore(maintenance): enable isolatedModules * chore: remove redundant comments from tsconfig * chore: changed path of tsbuild cache --- packages/idempotency/package.json | 2 +- packages/parameters/package.json | 2 +- packages/testing/package.json | 2 +- packages/tracer/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/idempotency/package.json b/packages/idempotency/package.json index 54664020f0..5984549a7d 100644 --- a/packages/idempotency/package.json +++ b/packages/idempotency/package.json @@ -136,4 +136,4 @@ "aws-sdk-client-mock": "^3.0.1", "aws-sdk-client-mock-jest": "^3.0.1" } -} \ No newline at end of file +} diff --git a/packages/parameters/package.json b/packages/parameters/package.json index 2469a717ee..ba05a0d60b 100644 --- a/packages/parameters/package.json +++ b/packages/parameters/package.json @@ -198,4 +198,4 @@ "optional": true } } -} \ No newline at end of file +} diff --git a/packages/testing/package.json b/packages/testing/package.json index 66aea8a0a3..0e470554b9 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -100,4 +100,4 @@ "aws-cdk-lib": "^2.130.0", "esbuild": "^0.20.1" } -} \ No newline at end of file +} diff --git a/packages/tracer/package.json b/packages/tracer/package.json index 1832af85c7..a88df7c2f6 100644 --- a/packages/tracer/package.json +++ b/packages/tracer/package.json @@ -105,4 +105,4 @@ "serverless", "nodejs" ] -} \ No newline at end of file +} From 0f29fea68f3159fba163398340f4dec162de663e Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 30 Oct 2023 23:25:19 +0100 Subject: [PATCH 020/103] fix: idempotency types --- packages/idempotency/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/idempotency/package.json b/packages/idempotency/package.json index 5984549a7d..54664020f0 100644 --- a/packages/idempotency/package.json +++ b/packages/idempotency/package.json @@ -136,4 +136,4 @@ "aws-sdk-client-mock": "^3.0.1", "aws-sdk-client-mock-jest": "^3.0.1" } -} +} \ No newline at end of file From 62756011af64a3319a63e8af5378ea62cb406ed1 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 31 Oct 2023 11:02:41 +0100 Subject: [PATCH 021/103] build(maintenance): bump aws sdk dev dependencies --- packages/idempotency/package.json | 2 +- packages/metrics/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/idempotency/package.json b/packages/idempotency/package.json index 54664020f0..5984549a7d 100644 --- a/packages/idempotency/package.json +++ b/packages/idempotency/package.json @@ -136,4 +136,4 @@ "aws-sdk-client-mock": "^3.0.1", "aws-sdk-client-mock-jest": "^3.0.1" } -} \ No newline at end of file +} diff --git a/packages/metrics/package.json b/packages/metrics/package.json index 31c511cc4e..0c71584273 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -100,4 +100,4 @@ "serverless", "nodejs" ] -} \ No newline at end of file +} From 2931c5adc4ee762fb6e1a1194f54588b031808ff Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Thu, 2 Nov 2023 17:42:52 +0100 Subject: [PATCH 022/103] fix(metrics): deduplicate dimensions when serialising (#1780) * fix: deduplicate dimensions when serializing * fix tests * remove tsbuildinfo * remove whitespace * fix gitignore again * play some sonar games * fix test --- packages/metrics/tests/unit/Metrics.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index a1ee97ac4a..cc4259a2d1 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -1530,7 +1530,11 @@ describe('Class: Metrics', () => { }); // Act +<<<<<<< HEAD metrics.addMetric(testMetric, MetricUnit.Count, 10); +======= + metrics.addMetric(testMetric, MetricUnits.Count, 10); +>>>>>>> 9b9fc927 (fix(metrics): deduplicate dimensions when serialising (#1780)) metrics.addDimension('foo', 'baz'); const loggedData = metrics.serializeMetrics(); @@ -1549,7 +1553,11 @@ describe('Class: Metrics', () => { Metrics: [ { Name: testMetric, +<<<<<<< HEAD Unit: MetricUnit.Count, +======= + Unit: MetricUnits.Count, +>>>>>>> 9b9fc927 (fix(metrics): deduplicate dimensions when serialising (#1780)) }, ], Namespace: TEST_NAMESPACE, From e32f74a6352494ad0d986b7cb9d85b5a5ac576d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 3 Nov 2023 11:01:10 +0000 Subject: [PATCH 023/103] chore(release): v1.14.2 [skip ci] --- packages/commons/package.json | 2 +- packages/idempotency/package.json | 2 +- packages/metrics/package.json | 2 +- packages/metrics/tests/unit/Metrics.test.ts | 8 -------- packages/parameters/package.json | 2 +- packages/testing/package.json | 2 +- packages/tracer/package.json | 2 +- 7 files changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/commons/package.json b/packages/commons/package.json index aac76fecea..77a2908835 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -75,4 +75,4 @@ "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing" } -} +} \ No newline at end of file diff --git a/packages/idempotency/package.json b/packages/idempotency/package.json index 5984549a7d..54664020f0 100644 --- a/packages/idempotency/package.json +++ b/packages/idempotency/package.json @@ -136,4 +136,4 @@ "aws-sdk-client-mock": "^3.0.1", "aws-sdk-client-mock-jest": "^3.0.1" } -} +} \ No newline at end of file diff --git a/packages/metrics/package.json b/packages/metrics/package.json index 0c71584273..31c511cc4e 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -100,4 +100,4 @@ "serverless", "nodejs" ] -} +} \ No newline at end of file diff --git a/packages/metrics/tests/unit/Metrics.test.ts b/packages/metrics/tests/unit/Metrics.test.ts index cc4259a2d1..a1ee97ac4a 100644 --- a/packages/metrics/tests/unit/Metrics.test.ts +++ b/packages/metrics/tests/unit/Metrics.test.ts @@ -1530,11 +1530,7 @@ describe('Class: Metrics', () => { }); // Act -<<<<<<< HEAD metrics.addMetric(testMetric, MetricUnit.Count, 10); -======= - metrics.addMetric(testMetric, MetricUnits.Count, 10); ->>>>>>> 9b9fc927 (fix(metrics): deduplicate dimensions when serialising (#1780)) metrics.addDimension('foo', 'baz'); const loggedData = metrics.serializeMetrics(); @@ -1553,11 +1549,7 @@ describe('Class: Metrics', () => { Metrics: [ { Name: testMetric, -<<<<<<< HEAD Unit: MetricUnit.Count, -======= - Unit: MetricUnits.Count, ->>>>>>> 9b9fc927 (fix(metrics): deduplicate dimensions when serialising (#1780)) }, ], Namespace: TEST_NAMESPACE, diff --git a/packages/parameters/package.json b/packages/parameters/package.json index ba05a0d60b..2469a717ee 100644 --- a/packages/parameters/package.json +++ b/packages/parameters/package.json @@ -198,4 +198,4 @@ "optional": true } } -} +} \ No newline at end of file diff --git a/packages/testing/package.json b/packages/testing/package.json index 0e470554b9..66aea8a0a3 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -100,4 +100,4 @@ "aws-cdk-lib": "^2.130.0", "esbuild": "^0.20.1" } -} +} \ No newline at end of file diff --git a/packages/tracer/package.json b/packages/tracer/package.json index a88df7c2f6..1832af85c7 100644 --- a/packages/tracer/package.json +++ b/packages/tracer/package.json @@ -105,4 +105,4 @@ "serverless", "nodejs" ] -} +} \ No newline at end of file From 8cef57de637e90259b6329607b34790be984f4f1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 14 Nov 2023 11:25:13 +0000 Subject: [PATCH 024/103] chore(release): v1.15.0 [skip ci] --- examples/cdk/CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/cdk/CHANGELOG.md b/examples/cdk/CHANGELOG.md index 2b94cf69f4..6e790a8e38 100644 --- a/examples/cdk/CHANGELOG.md +++ b/examples/cdk/CHANGELOG.md @@ -45,12 +45,22 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline - **logger:** add support for `AWS_LAMBDA_LOG_LEVEL` and `POWERTOOLS_LOG_LEVEL` ([#1795](https://github.com/aws-powertools/powertools-lambda-typescript/issues/1795)) ([e69abfb](https://github.com/aws-powertools/powertools-lambda-typescript/commit/e69abfb5f1b5d3bf993ac2fe66fae85a85af9905)) +======= +>>>>>>> 140c1282 (chore(release): v1.15.0 [skip ci]) # [1.15.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v1.14.2...v1.15.0) (2023-11-14) ### Features - **maintenance:** add support for nodejs20.x runtime ([#1790](https://github.com/aws-powertools/powertools-lambda-typescript/issues/1790)) ([6b9b1bc](https://github.com/aws-powertools/powertools-lambda-typescript/commit/6b9b1bcb9baf4b3d8e0e5ec6709594aca09bb033)) +<<<<<<< HEAD +======= +<<<<<<< HEAD +======= +>>>>>>> 38bc54f0 (chore(release): v1.14.2 [skip ci]) +======= +>>>>>>> 140c1282 (chore(release): v1.15.0 [skip ci]) +>>>>>>> 85c6da66 (chore(release): v1.15.0 [skip ci]) ## [1.14.2](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v1.14.1...v1.14.2) (2023-11-03) **Note:** Version bump only for package cdk-sample From 12bd0e0be21bd772f5598385a9642d3b6278ee82 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 16 Nov 2023 15:20:16 +0000 Subject: [PATCH 025/103] chore(release): v1.16.0 [skip ci] --- examples/cdk/CHANGELOG.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/examples/cdk/CHANGELOG.md b/examples/cdk/CHANGELOG.md index 6e790a8e38..2b94cf69f4 100644 --- a/examples/cdk/CHANGELOG.md +++ b/examples/cdk/CHANGELOG.md @@ -45,22 +45,12 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline - **logger:** add support for `AWS_LAMBDA_LOG_LEVEL` and `POWERTOOLS_LOG_LEVEL` ([#1795](https://github.com/aws-powertools/powertools-lambda-typescript/issues/1795)) ([e69abfb](https://github.com/aws-powertools/powertools-lambda-typescript/commit/e69abfb5f1b5d3bf993ac2fe66fae85a85af9905)) -======= ->>>>>>> 140c1282 (chore(release): v1.15.0 [skip ci]) # [1.15.0](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v1.14.2...v1.15.0) (2023-11-14) ### Features - **maintenance:** add support for nodejs20.x runtime ([#1790](https://github.com/aws-powertools/powertools-lambda-typescript/issues/1790)) ([6b9b1bc](https://github.com/aws-powertools/powertools-lambda-typescript/commit/6b9b1bcb9baf4b3d8e0e5ec6709594aca09bb033)) -<<<<<<< HEAD -======= -<<<<<<< HEAD -======= ->>>>>>> 38bc54f0 (chore(release): v1.14.2 [skip ci]) -======= ->>>>>>> 140c1282 (chore(release): v1.15.0 [skip ci]) ->>>>>>> 85c6da66 (chore(release): v1.15.0 [skip ci]) ## [1.14.2](https://github.com/aws-powertools/powertools-lambda-typescript/compare/v1.14.1...v1.14.2) (2023-11-03) **Note:** Version bump only for package cdk-sample From ec76a4fcb0a70a17e1bc7a6be3510bfccf58a952 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 24 Nov 2023 14:07:00 +0100 Subject: [PATCH 026/103] chore(maintenance): drop support for Node.js 14 (#1802) --- package.json | 2 +- packages/logger/src/formatter/LogFormatter.ts | 7 ++++--- tsconfig.json | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c1549c894e..e30fa7c00f 100644 --- a/package.json +++ b/package.json @@ -74,4 +74,4 @@ "engines": { "node": ">=16" } -} +} \ No newline at end of file diff --git a/packages/logger/src/formatter/LogFormatter.ts b/packages/logger/src/formatter/LogFormatter.ts index 48016377a7..dd874732c4 100644 --- a/packages/logger/src/formatter/LogFormatter.ts +++ b/packages/logger/src/formatter/LogFormatter.ts @@ -65,10 +65,11 @@ abstract class LogFormatter implements LogFormatterInterface { location: this.getCodeLocation(error.stack), message: error.message, stack: error.stack, - cause: - error.cause instanceof Error + cause: isErrorWithCause(error) + ? error.cause instanceof Error ? this.formatError(error.cause) - : error.cause, + : error.cause + : undefined, }; } diff --git a/tsconfig.json b/tsconfig.json index 08d5af9848..06b00be287 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "incremental": true, "composite": true, - "target": "ES2022", // Node.js 16 + "target": "ES2021", // Node.js 16 "experimentalDecorators": true, "module": "commonjs", "moduleResolution": "node", From 0fe1db117b0e4e030e9ec06fee95218a0f2bfe76 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 24 Nov 2023 11:24:19 -0800 Subject: [PATCH 027/103] chore: package lock --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index b9a5b8edda..48e25c231d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17331,4 +17331,4 @@ } } } -} +} \ No newline at end of file From 7240afa182b3a50cac9cacef04937e5bdc30ccaa Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 20 Dec 2023 18:50:22 +0800 Subject: [PATCH 028/103] chore(maintenance): add --require-hashes flag to pip installs (#1827) --- .github/workflows/reusable_publish_docs.yml | 4 ++ docs/requirements.txt | 55 +++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/.github/workflows/reusable_publish_docs.yml b/.github/workflows/reusable_publish_docs.yml index 3b8fc275d2..2659a92ba9 100644 --- a/.github/workflows/reusable_publish_docs.yml +++ b/.github/workflows/reusable_publish_docs.yml @@ -65,6 +65,10 @@ jobs: - name: Install doc generation dependencies run: | pip install --require-hashes -r docs/requirements.txt + - name: Setup doc deploy + run: | + git config --global user.name Docs deploy + git config --global user.email aws-devax-open-source@amazon.com - name: Git refresh tip (detached mode) # Git Detached mode (release notes) doesn't have origin if: ${{ inputs.detached_mode }} diff --git a/docs/requirements.txt b/docs/requirements.txt index 2da9bfcd0b..5a87e265fe 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -108,10 +108,13 @@ click==8.1.7 \ --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de # via mkdocs +<<<<<<< HEAD colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via mkdocs-material +======= +>>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) ghp-import==2.1.0 \ --hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \ --hash=sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343 @@ -120,6 +123,7 @@ gitdb==4.0.11 \ --hash=sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4 \ --hash=sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b # via gitpython +<<<<<<< HEAD gitpython==3.1.41 \ --hash=sha256:c36b6634d069b3f719610175020a9aed919421c87552185b085e04fbbdb10b7c \ --hash=sha256:ed66e624884f76df22c8e16066d567aaa5a37d5b5fa19db2c6df6f7156db9048 @@ -143,6 +147,22 @@ markdown==3.5.1 \ # mkdocs # mkdocs-material # pymdown-extensions +======= +gitpython==3.1.40 \ + --hash=sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4 \ + --hash=sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a + # via mkdocs-git-revision-date-plugin +jinja2==3.1.2 \ + --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ + --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 + # via + # mkdocs + # mkdocs-git-revision-date-plugin +markdown==3.5.1 \ + --hash=sha256:5874b47d4ee3f0b14d764324d2c94c03ea66bee56f2d929da9f2508d65e722dc \ + --hash=sha256:b65d7beb248dc22f2e8a31fb706d93798093c308dc1aba295aedeb9d41a813bd + # via mkdocs +>>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) markupsafe==2.1.3 \ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ @@ -211,14 +231,18 @@ mergedeep==1.3.4 \ --hash=sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8 \ --hash=sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307 # via mkdocs +<<<<<<< HEAD mike==1.1.2 \ --hash=sha256:4c307c28769834d78df10f834f57f810f04ca27d248f80a75f49c6fa2d1527ca \ --hash=sha256:56c3f1794c2d0b5fdccfa9b9487beb013ca813de2e3ad0744724e9d34d40b77b # via -r requirements.in +======= +>>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) mkdocs==1.5.3 \ --hash=sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1 \ --hash=sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2 # via +<<<<<<< HEAD # mike # mkdocs-exclude # mkdocs-git-revision-date-plugin @@ -237,10 +261,21 @@ mkdocs-material-extensions==1.3.1 \ --hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \ --hash=sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31 # via mkdocs-material +======= + # mkdocs-exclude + # mkdocs-git-revision-date-plugin +mkdocs-exclude==1.0.2 \ + --hash=sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51 + # via -r docker-requirements.txt +mkdocs-git-revision-date-plugin==0.3.2 \ + --hash=sha256:2e67956cb01823dd2418e2833f3623dee8604cdf223bddd005fe36226a56f6ef + # via -r docker-requirements.txt +>>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) packaging==23.2 \ --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 # via mkdocs +<<<<<<< HEAD paginate==0.5.6 \ --hash=sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d # via mkdocs-material @@ -260,6 +295,16 @@ pymdown-extensions==10.5 \ --hash=sha256:1b60f1e462adbec5a1ed79dac91f666c9c0d241fa294de1989f29d20096cfd0b \ --hash=sha256:1f0ca8bb5beff091315f793ee17683bc1390731f6ac4c5eb01e27464b80fe879 # via mkdocs-material +======= +pathspec==0.11.2 \ + --hash=sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20 \ + --hash=sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3 + # via mkdocs +platformdirs==4.0.0 \ + --hash=sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b \ + --hash=sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731 + # via mkdocs +>>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) python-dateutil==2.8.2 \ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 @@ -316,14 +361,19 @@ pyyaml==6.0.1 \ --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f # via +<<<<<<< HEAD # mike # mkdocs # pymdown-extensions +======= + # mkdocs +>>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) # pyyaml-env-tag pyyaml-env-tag==0.1 \ --hash=sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb \ --hash=sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069 # via mkdocs +<<<<<<< HEAD regex==2023.10.3 \ --hash=sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a \ --hash=sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07 \ @@ -418,6 +468,8 @@ requests==2.31.0 \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 # via mkdocs-material +======= +>>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 @@ -426,6 +478,7 @@ smmap==5.0.1 \ --hash=sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62 \ --hash=sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da # via gitdb +<<<<<<< HEAD urllib3==2.1.0 \ --hash=sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3 \ --hash=sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54 @@ -434,6 +487,8 @@ verspec==0.1.0 \ --hash=sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31 \ --hash=sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e # via mike +======= +>>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) watchdog==3.0.0 \ --hash=sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a \ --hash=sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100 \ From e5d271d42dd41184cc51b8266d1fa694179f5f3c Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Thu, 21 Dec 2023 15:36:39 +0100 Subject: [PATCH 029/103] bump version to 9.5.2, rerun pip-compile with correct deps (#1830) --- docs/requirements.txt | 57 +------------------------------------------ package-lock.json | 2 +- 2 files changed, 2 insertions(+), 57 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 5a87e265fe..eea4e747b1 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -108,13 +108,10 @@ click==8.1.7 \ --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de # via mkdocs -<<<<<<< HEAD colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via mkdocs-material -======= ->>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) ghp-import==2.1.0 \ --hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \ --hash=sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343 @@ -123,7 +120,6 @@ gitdb==4.0.11 \ --hash=sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4 \ --hash=sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b # via gitpython -<<<<<<< HEAD gitpython==3.1.41 \ --hash=sha256:c36b6634d069b3f719610175020a9aed919421c87552185b085e04fbbdb10b7c \ --hash=sha256:ed66e624884f76df22c8e16066d567aaa5a37d5b5fa19db2c6df6f7156db9048 @@ -147,22 +143,6 @@ markdown==3.5.1 \ # mkdocs # mkdocs-material # pymdown-extensions -======= -gitpython==3.1.40 \ - --hash=sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4 \ - --hash=sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a - # via mkdocs-git-revision-date-plugin -jinja2==3.1.2 \ - --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ - --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 - # via - # mkdocs - # mkdocs-git-revision-date-plugin -markdown==3.5.1 \ - --hash=sha256:5874b47d4ee3f0b14d764324d2c94c03ea66bee56f2d929da9f2508d65e722dc \ - --hash=sha256:b65d7beb248dc22f2e8a31fb706d93798093c308dc1aba295aedeb9d41a813bd - # via mkdocs ->>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) markupsafe==2.1.3 \ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ @@ -231,18 +211,14 @@ mergedeep==1.3.4 \ --hash=sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8 \ --hash=sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307 # via mkdocs -<<<<<<< HEAD mike==1.1.2 \ --hash=sha256:4c307c28769834d78df10f834f57f810f04ca27d248f80a75f49c6fa2d1527ca \ --hash=sha256:56c3f1794c2d0b5fdccfa9b9487beb013ca813de2e3ad0744724e9d34d40b77b # via -r requirements.in -======= ->>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) mkdocs==1.5.3 \ --hash=sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1 \ --hash=sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2 # via -<<<<<<< HEAD # mike # mkdocs-exclude # mkdocs-git-revision-date-plugin @@ -261,21 +237,10 @@ mkdocs-material-extensions==1.3.1 \ --hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \ --hash=sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31 # via mkdocs-material -======= - # mkdocs-exclude - # mkdocs-git-revision-date-plugin -mkdocs-exclude==1.0.2 \ - --hash=sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51 - # via -r docker-requirements.txt -mkdocs-git-revision-date-plugin==0.3.2 \ - --hash=sha256:2e67956cb01823dd2418e2833f3623dee8604cdf223bddd005fe36226a56f6ef - # via -r docker-requirements.txt ->>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) packaging==23.2 \ --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 # via mkdocs -<<<<<<< HEAD paginate==0.5.6 \ --hash=sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d # via mkdocs-material @@ -295,16 +260,6 @@ pymdown-extensions==10.5 \ --hash=sha256:1b60f1e462adbec5a1ed79dac91f666c9c0d241fa294de1989f29d20096cfd0b \ --hash=sha256:1f0ca8bb5beff091315f793ee17683bc1390731f6ac4c5eb01e27464b80fe879 # via mkdocs-material -======= -pathspec==0.11.2 \ - --hash=sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20 \ - --hash=sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3 - # via mkdocs -platformdirs==4.0.0 \ - --hash=sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b \ - --hash=sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731 - # via mkdocs ->>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) python-dateutil==2.8.2 \ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 @@ -361,19 +316,14 @@ pyyaml==6.0.1 \ --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f # via -<<<<<<< HEAD # mike # mkdocs # pymdown-extensions -======= - # mkdocs ->>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) # pyyaml-env-tag pyyaml-env-tag==0.1 \ --hash=sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb \ --hash=sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069 # via mkdocs -<<<<<<< HEAD regex==2023.10.3 \ --hash=sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a \ --hash=sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07 \ @@ -468,8 +418,6 @@ requests==2.31.0 \ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 # via mkdocs-material -======= ->>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 @@ -478,7 +426,6 @@ smmap==5.0.1 \ --hash=sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62 \ --hash=sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da # via gitdb -<<<<<<< HEAD urllib3==2.1.0 \ --hash=sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3 \ --hash=sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54 @@ -487,8 +434,6 @@ verspec==0.1.0 \ --hash=sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31 \ --hash=sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e # via mike -======= ->>>>>>> befbedfe (chore(maintenance): add --require-hashes flag to pip installs (#1827)) watchdog==3.0.0 \ --hash=sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a \ --hash=sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100 \ @@ -517,4 +462,4 @@ watchdog==3.0.0 \ --hash=sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64 \ --hash=sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44 \ --hash=sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33 - # via mkdocs + # via mkdocs \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 48e25c231d..b9a5b8edda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17331,4 +17331,4 @@ } } } -} \ No newline at end of file +} From 32f5835409a576917203566fa19db085ab0e2302 Mon Sep 17 00:00:00 2001 From: Alexander Schueren Date: Tue, 9 Jan 2024 16:51:02 +0100 Subject: [PATCH 030/103] chore(ci): Dependabot fine tuning (#1862) * ignore major updates for mike * set versioning strategy for cdk, dependabot is running on auto upgrade strategy per default * ignore middy major upgrades * remove CodeQL, it's enough to have Sonar * update package-lock --- .github/dependabot.yml | 76 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b83c97aea3..37748f3d39 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -27,24 +27,72 @@ updates: - dependency-name: "mike" update-types: ["version-update:semver-major"] + - package-ecosystem: npm + directory: /docs/snippets + schedule: + interval: daily + + - package-ecosystem: npm + directory: /examples/cdk + schedule: + interval: daily + + + - package-ecosystem: npm + directory: /examples/sam + schedule: + interval: daily + + - package-ecosystem: npm + directory: /layers + schedule: + interval: daily + - package-ecosystem: npm directory: / labels: [ ] schedule: interval: daily - versioning-strategy: increase ignore: - dependency-name: "@middy/core" - update-types: [ "version-update:semver-major" ] - groups: - aws-sdk: - patterns: - - "@aws-sdk/**" - - "@smithy/**" - - "aws-sdk-client-mock" - - "aws-sdk-client-mock-jest" - aws-cdk: - patterns: - - "@aws-cdk/**" - - "aws-cdk-lib" - - "aws-cdk" + update-types: ["version-update:semver-major"] + + - package-ecosystem: npm + directory: /packages/batch + schedule: + interval: daily + + - package-ecosystem: npm + directory: /packages/commons + schedule: + interval: daily + + - package-ecosystem: npm + directory: /packages/idempotency + schedule: + interval: daily + + - package-ecosystem: npm + directory: /packages/logger + schedule: + interval: daily + + - package-ecosystem: npm + directory: /packages/metrics + schedule: + interval: daily + + - package-ecosystem: npm + directory: /packages/parameters + schedule: + interval: daily + + - package-ecosystem: npm + directory: /packages/testing + schedule: + interval: daily + + - package-ecosystem: npm + directory: /packages/tracer + schedule: + interval: daily From 6ed197b6019cc0786c373b94e911e81dceb21a87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:11:10 +0000 Subject: [PATCH 031/103] chore(deps-dev): bump @aws-sdk/client-cloudwatch from 3.438.0 to 3.485.0 (#1857) --- package-lock.json | 427 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 427 insertions(+) diff --git a/package-lock.json b/package-lock.json index b9a5b8edda..1b5f804207 100644 --- a/package-lock.json +++ b/package-lock.json @@ -418,6 +418,433 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/client-sso": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.485.0.tgz", + "integrity": "sha512-apN2bEn0PZs0jD4jAfvwO3dlWqw9YIQJ6TAudM1bd3S5vzWqlBBcLfQpK6taHoQaI+WqgUWXLuOf7gRFbGXKPg==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.485.0", + "@aws-sdk/middleware-host-header": "3.485.0", + "@aws-sdk/middleware-logger": "3.485.0", + "@aws-sdk/middleware-recursion-detection": "3.485.0", + "@aws-sdk/middleware-user-agent": "3.485.0", + "@aws-sdk/region-config-resolver": "3.485.0", + "@aws-sdk/types": "3.485.0", + "@aws-sdk/util-endpoints": "3.485.0", + "@aws-sdk/util-user-agent-browser": "3.485.0", + "@aws-sdk/util-user-agent-node": "3.485.0", + "@smithy/config-resolver": "^2.0.23", + "@smithy/core": "^1.2.2", + "@smithy/fetch-http-handler": "^2.3.2", + "@smithy/hash-node": "^2.0.18", + "@smithy/invalid-dependency": "^2.0.16", + "@smithy/middleware-content-length": "^2.0.18", + "@smithy/middleware-endpoint": "^2.3.0", + "@smithy/middleware-retry": "^2.0.26", + "@smithy/middleware-serde": "^2.0.16", + "@smithy/middleware-stack": "^2.0.10", + "@smithy/node-config-provider": "^2.1.9", + "@smithy/node-http-handler": "^2.2.2", + "@smithy/protocol-http": "^3.0.12", + "@smithy/smithy-client": "^2.2.1", + "@smithy/types": "^2.8.0", + "@smithy/url-parser": "^2.0.16", + "@smithy/util-base64": "^2.0.1", + "@smithy/util-body-length-browser": "^2.0.1", + "@smithy/util-body-length-node": "^2.1.0", + "@smithy/util-defaults-mode-browser": "^2.0.24", + "@smithy/util-defaults-mode-node": "^2.0.32", + "@smithy/util-endpoints": "^1.0.8", + "@smithy/util-retry": "^2.0.9", + "@smithy/util-utf8": "^2.0.2", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/client-sts": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.485.0.tgz", + "integrity": "sha512-PI4q36kVF0fpIPZyeQhrwwJZ6SRkOGvU3rX5Qn4b5UY5X+Ct1aLhqSX8/OB372UZIcnh6eSvERu8POHleDO7Jw==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.485.0", + "@aws-sdk/credential-provider-node": "3.485.0", + "@aws-sdk/middleware-host-header": "3.485.0", + "@aws-sdk/middleware-logger": "3.485.0", + "@aws-sdk/middleware-recursion-detection": "3.485.0", + "@aws-sdk/middleware-user-agent": "3.485.0", + "@aws-sdk/region-config-resolver": "3.485.0", + "@aws-sdk/types": "3.485.0", + "@aws-sdk/util-endpoints": "3.485.0", + "@aws-sdk/util-user-agent-browser": "3.485.0", + "@aws-sdk/util-user-agent-node": "3.485.0", + "@smithy/config-resolver": "^2.0.23", + "@smithy/core": "^1.2.2", + "@smithy/fetch-http-handler": "^2.3.2", + "@smithy/hash-node": "^2.0.18", + "@smithy/invalid-dependency": "^2.0.16", + "@smithy/middleware-content-length": "^2.0.18", + "@smithy/middleware-endpoint": "^2.3.0", + "@smithy/middleware-retry": "^2.0.26", + "@smithy/middleware-serde": "^2.0.16", + "@smithy/middleware-stack": "^2.0.10", + "@smithy/node-config-provider": "^2.1.9", + "@smithy/node-http-handler": "^2.2.2", + "@smithy/protocol-http": "^3.0.12", + "@smithy/smithy-client": "^2.2.1", + "@smithy/types": "^2.8.0", + "@smithy/url-parser": "^2.0.16", + "@smithy/util-base64": "^2.0.1", + "@smithy/util-body-length-browser": "^2.0.1", + "@smithy/util-body-length-node": "^2.1.0", + "@smithy/util-defaults-mode-browser": "^2.0.24", + "@smithy/util-defaults-mode-node": "^2.0.32", + "@smithy/util-endpoints": "^1.0.8", + "@smithy/util-middleware": "^2.0.9", + "@smithy/util-retry": "^2.0.9", + "@smithy/util-utf8": "^2.0.2", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/core": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.485.0.tgz", + "integrity": "sha512-Yvi80DQcbjkYCft471ClE3HuetuNVqntCs6eFOomDcrJaqdOFrXv2kJAxky84MRA/xb7bGlDGAPbTuj1ICputg==", + "dev": true, + "dependencies": { + "@smithy/core": "^1.2.2", + "@smithy/protocol-http": "^3.0.12", + "@smithy/signature-v4": "^2.0.0", + "@smithy/smithy-client": "^2.2.1", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.485.0.tgz", + "integrity": "sha512-3XkFgwVU1XOB33dV7t9BKJ/ptdl2iS+0dxE7ecq8aqT2/gsfKmLCae1G17P8WmdD3z0kMDTvnqM2aWgUnSOkmg==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.485.0.tgz", + "integrity": "sha512-cFYF/Bdw7EnT4viSxYpNIv3IBkri/Yb+JpQXl8uDq7bfVJfAN5qZmK07vRkg08xL6TC4F41wshhMSAucGdTwIw==", + "dev": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "3.485.0", + "@aws-sdk/credential-provider-process": "3.485.0", + "@aws-sdk/credential-provider-sso": "3.485.0", + "@aws-sdk/credential-provider-web-identity": "3.485.0", + "@aws-sdk/types": "3.485.0", + "@smithy/credential-provider-imds": "^2.0.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.485.0.tgz", + "integrity": "sha512-2DwzO2azkSzngifKDT61W/DL0tSzewuaFHiLJWdfc8Et3mdAQJ9x3KAj8u7XFpjIcGNqk7FiKjN+zeGUuNiEhA==", + "dev": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "3.485.0", + "@aws-sdk/credential-provider-ini": "3.485.0", + "@aws-sdk/credential-provider-process": "3.485.0", + "@aws-sdk/credential-provider-sso": "3.485.0", + "@aws-sdk/credential-provider-web-identity": "3.485.0", + "@aws-sdk/types": "3.485.0", + "@smithy/credential-provider-imds": "^2.0.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.485.0.tgz", + "integrity": "sha512-X9qS6ZO/rDKYDgWqD1YmSX7sAUUHax9HbXlgGiTTdtfhZvQh1ZmnH6wiPu5WNliafHZFtZT2W07kgrDLPld/Ug==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.485.0.tgz", + "integrity": "sha512-l0oC8GTrWh+LFQQfSmG1Jai1PX7Mhj9arb/CaS1/tmeZE0hgIXW++tvljYs/Dds4LGXUlaWG+P7BrObf6OyIXA==", + "dev": true, + "dependencies": { + "@aws-sdk/client-sso": "3.485.0", + "@aws-sdk/token-providers": "3.485.0", + "@aws-sdk/types": "3.485.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.485.0.tgz", + "integrity": "sha512-WpBFZFE0iXtnibH5POMEKITj/hR0YV5l2n9p8BEvKjdJ63s3Xke1RN20ZdIyKDaRDwj8adnKDgNPEnAKdS4kLw==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.485.0.tgz", + "integrity": "sha512-1mAUX9dQNGo2RIKseVj7SI/D5abQJQ/Os8hQ0NyVAyyVYF+Yjx5PphKgfhM5yoBwuwZUl6q71XPYEGNx7be6SA==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/protocol-http": "^3.0.12", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-logger": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.485.0.tgz", + "integrity": "sha512-O8IgJ0LHi5wTs5GlpI7nqmmSSagkVdd1shpGgQWY2h0kMSCII8CJZHBG97dlFFpGTvx5EDlhPNek7rl/6F4dRw==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.485.0.tgz", + "integrity": "sha512-ZeVNATGNFcqkWDut3luVszROTUzkU5u+rJpB/xmeMoenlDAjPRiHt/ca3WkI5wAnIJ1VSNGpD2sOFLMCH+EWag==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/protocol-http": "^3.0.12", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-signing": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.485.0.tgz", + "integrity": "sha512-41xzT2p1sOibhsLkdE5rwPJkNbBtKD8Gp36/ySfu0KE415wfXKacElSVxAaBw39/j7iSWDYqqybeEYbAzk+3GQ==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/property-provider": "^2.0.0", + "@smithy/protocol-http": "^3.0.12", + "@smithy/signature-v4": "^2.0.0", + "@smithy/types": "^2.8.0", + "@smithy/util-middleware": "^2.0.9", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.485.0.tgz", + "integrity": "sha512-CddCVOn+OPQ0CcchketIg+WF6v+MDLAf3GOYTR2htUxxIm7HABuRd6R3kvQ5Jny9CV8gMt22G1UZITsFexSJlQ==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@aws-sdk/util-endpoints": "3.485.0", + "@smithy/protocol-http": "^3.0.12", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.485.0.tgz", + "integrity": "sha512-2FB2EQ0sIE+YgFqGtkE1lDIMIL6nYe6MkOHBwBM7bommadKIrbbr2L22bPZGs3ReTsxiJabjzxbuCAVhrpHmhg==", + "dev": true, + "dependencies": { + "@smithy/node-config-provider": "^2.1.9", + "@smithy/types": "^2.8.0", + "@smithy/util-config-provider": "^2.1.0", + "@smithy/util-middleware": "^2.0.9", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/token-providers": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.485.0.tgz", + "integrity": "sha512-kOXA1WKIVIFNRqHL8ynVZ3hCKLsgnEmGr2iDR6agDNw5fYIlCO/6N2xR6QdGcLTvUUbwOlz4OvKLUQnWMKAnnA==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/middleware-host-header": "3.485.0", + "@aws-sdk/middleware-logger": "3.485.0", + "@aws-sdk/middleware-recursion-detection": "3.485.0", + "@aws-sdk/middleware-user-agent": "3.485.0", + "@aws-sdk/region-config-resolver": "3.485.0", + "@aws-sdk/types": "3.485.0", + "@aws-sdk/util-endpoints": "3.485.0", + "@aws-sdk/util-user-agent-browser": "3.485.0", + "@aws-sdk/util-user-agent-node": "3.485.0", + "@smithy/config-resolver": "^2.0.23", + "@smithy/fetch-http-handler": "^2.3.2", + "@smithy/hash-node": "^2.0.18", + "@smithy/invalid-dependency": "^2.0.16", + "@smithy/middleware-content-length": "^2.0.18", + "@smithy/middleware-endpoint": "^2.3.0", + "@smithy/middleware-retry": "^2.0.26", + "@smithy/middleware-serde": "^2.0.16", + "@smithy/middleware-stack": "^2.0.10", + "@smithy/node-config-provider": "^2.1.9", + "@smithy/node-http-handler": "^2.2.2", + "@smithy/property-provider": "^2.0.0", + "@smithy/protocol-http": "^3.0.12", + "@smithy/shared-ini-file-loader": "^2.0.6", + "@smithy/smithy-client": "^2.2.1", + "@smithy/types": "^2.8.0", + "@smithy/url-parser": "^2.0.16", + "@smithy/util-base64": "^2.0.1", + "@smithy/util-body-length-browser": "^2.0.1", + "@smithy/util-body-length-node": "^2.1.0", + "@smithy/util-defaults-mode-browser": "^2.0.24", + "@smithy/util-defaults-mode-node": "^2.0.32", + "@smithy/util-endpoints": "^1.0.8", + "@smithy/util-retry": "^2.0.9", + "@smithy/util-utf8": "^2.0.2", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/types": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.485.0.tgz", + "integrity": "sha512-+QW32YQdvZRDOwrAQPo/qCyXoSjgXB6RwJwCwkd8ebJXRXw6tmGKIHaZqYHt/LtBymvnaBgBBADNa4+qFvlOFw==", + "dev": true, + "dependencies": { + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-endpoints": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.485.0.tgz", + "integrity": "sha512-dTd642F7nJisApF8YjniqQ6U59CP/DCtar11fXf1nG9YNBCBsNNVw5ZfZb5nSNzaIdy27mQioWTCV18JEj1mxg==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/util-endpoints": "^1.0.8", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.485.0.tgz", + "integrity": "sha512-QliWbjg0uOhGTcWgWTKPMY0SBi07g253DjwrCINT1auqDrdQPxa10xozpZExBYjAK2KuhYDNUzni127ae6MHOw==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/types": "^2.8.0", + "bowser": "^2.11.0", + "tslib": "^2.5.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.485.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.485.0.tgz", + "integrity": "sha512-QF+aQ9jnDlPUlFBxBRqOylPf86xQuD3aEPpOErR+50qJawVvKa94uiAFdvtI9jv6hnRZmuFsTj2rsyytnbAYBA==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.485.0", + "@smithy/node-config-provider": "^2.1.9", + "@smithy/types": "^2.8.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, "node_modules/@aws-sdk/client-dynamodb": { "version": "3.525.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.525.0.tgz", From d9efd4aca8c3edf89f95a8d2d58f5877a62c429b Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 16 Jan 2024 23:44:45 +0800 Subject: [PATCH 032/103] chore(deps): fix dependencies and dependabot config (#1917) --- .github/dependabot.yml | 74 ++----- package-lock.json | 427 ----------------------------------------- 2 files changed, 12 insertions(+), 489 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 37748f3d39..fd4d533082 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -27,72 +27,22 @@ updates: - dependency-name: "mike" update-types: ["version-update:semver-major"] - - package-ecosystem: npm - directory: /docs/snippets - schedule: - interval: daily - - - package-ecosystem: npm - directory: /examples/cdk - schedule: - interval: daily - - - - package-ecosystem: npm - directory: /examples/sam - schedule: - interval: daily - - - package-ecosystem: npm - directory: /layers - schedule: - interval: daily - - package-ecosystem: npm directory: / labels: [ ] schedule: interval: daily + versioning-strategy: increase ignore: - dependency-name: "@middy/core" - update-types: ["version-update:semver-major"] - - - package-ecosystem: npm - directory: /packages/batch - schedule: - interval: daily - - - package-ecosystem: npm - directory: /packages/commons - schedule: - interval: daily - - - package-ecosystem: npm - directory: /packages/idempotency - schedule: - interval: daily - - - package-ecosystem: npm - directory: /packages/logger - schedule: - interval: daily - - - package-ecosystem: npm - directory: /packages/metrics - schedule: - interval: daily - - - package-ecosystem: npm - directory: /packages/parameters - schedule: - interval: daily - - - package-ecosystem: npm - directory: /packages/testing - schedule: - interval: daily - - - package-ecosystem: npm - directory: /packages/tracer - schedule: - interval: daily + update-types: [ "version-update:semver-major" ] + groups: + aws-sdk: + patterns: + - "@aws-sdk/**" + - "aws-sdk-client-mock" + - "aws-sdk-client-mock-jest" + aws-cdk: + patterns: + - "@aws-cdk/**" + - "aws-cdk-lib" diff --git a/package-lock.json b/package-lock.json index 1b5f804207..b9a5b8edda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -418,433 +418,6 @@ "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/client-sso": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.485.0.tgz", - "integrity": "sha512-apN2bEn0PZs0jD4jAfvwO3dlWqw9YIQJ6TAudM1bd3S5vzWqlBBcLfQpK6taHoQaI+WqgUWXLuOf7gRFbGXKPg==", - "dev": true, - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/core": "3.485.0", - "@aws-sdk/middleware-host-header": "3.485.0", - "@aws-sdk/middleware-logger": "3.485.0", - "@aws-sdk/middleware-recursion-detection": "3.485.0", - "@aws-sdk/middleware-user-agent": "3.485.0", - "@aws-sdk/region-config-resolver": "3.485.0", - "@aws-sdk/types": "3.485.0", - "@aws-sdk/util-endpoints": "3.485.0", - "@aws-sdk/util-user-agent-browser": "3.485.0", - "@aws-sdk/util-user-agent-node": "3.485.0", - "@smithy/config-resolver": "^2.0.23", - "@smithy/core": "^1.2.2", - "@smithy/fetch-http-handler": "^2.3.2", - "@smithy/hash-node": "^2.0.18", - "@smithy/invalid-dependency": "^2.0.16", - "@smithy/middleware-content-length": "^2.0.18", - "@smithy/middleware-endpoint": "^2.3.0", - "@smithy/middleware-retry": "^2.0.26", - "@smithy/middleware-serde": "^2.0.16", - "@smithy/middleware-stack": "^2.0.10", - "@smithy/node-config-provider": "^2.1.9", - "@smithy/node-http-handler": "^2.2.2", - "@smithy/protocol-http": "^3.0.12", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", - "@smithy/url-parser": "^2.0.16", - "@smithy/util-base64": "^2.0.1", - "@smithy/util-body-length-browser": "^2.0.1", - "@smithy/util-body-length-node": "^2.1.0", - "@smithy/util-defaults-mode-browser": "^2.0.24", - "@smithy/util-defaults-mode-node": "^2.0.32", - "@smithy/util-endpoints": "^1.0.8", - "@smithy/util-retry": "^2.0.9", - "@smithy/util-utf8": "^2.0.2", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/client-sts": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.485.0.tgz", - "integrity": "sha512-PI4q36kVF0fpIPZyeQhrwwJZ6SRkOGvU3rX5Qn4b5UY5X+Ct1aLhqSX8/OB372UZIcnh6eSvERu8POHleDO7Jw==", - "dev": true, - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/core": "3.485.0", - "@aws-sdk/credential-provider-node": "3.485.0", - "@aws-sdk/middleware-host-header": "3.485.0", - "@aws-sdk/middleware-logger": "3.485.0", - "@aws-sdk/middleware-recursion-detection": "3.485.0", - "@aws-sdk/middleware-user-agent": "3.485.0", - "@aws-sdk/region-config-resolver": "3.485.0", - "@aws-sdk/types": "3.485.0", - "@aws-sdk/util-endpoints": "3.485.0", - "@aws-sdk/util-user-agent-browser": "3.485.0", - "@aws-sdk/util-user-agent-node": "3.485.0", - "@smithy/config-resolver": "^2.0.23", - "@smithy/core": "^1.2.2", - "@smithy/fetch-http-handler": "^2.3.2", - "@smithy/hash-node": "^2.0.18", - "@smithy/invalid-dependency": "^2.0.16", - "@smithy/middleware-content-length": "^2.0.18", - "@smithy/middleware-endpoint": "^2.3.0", - "@smithy/middleware-retry": "^2.0.26", - "@smithy/middleware-serde": "^2.0.16", - "@smithy/middleware-stack": "^2.0.10", - "@smithy/node-config-provider": "^2.1.9", - "@smithy/node-http-handler": "^2.2.2", - "@smithy/protocol-http": "^3.0.12", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", - "@smithy/url-parser": "^2.0.16", - "@smithy/util-base64": "^2.0.1", - "@smithy/util-body-length-browser": "^2.0.1", - "@smithy/util-body-length-node": "^2.1.0", - "@smithy/util-defaults-mode-browser": "^2.0.24", - "@smithy/util-defaults-mode-node": "^2.0.32", - "@smithy/util-endpoints": "^1.0.8", - "@smithy/util-middleware": "^2.0.9", - "@smithy/util-retry": "^2.0.9", - "@smithy/util-utf8": "^2.0.2", - "fast-xml-parser": "4.2.5", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/core": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.485.0.tgz", - "integrity": "sha512-Yvi80DQcbjkYCft471ClE3HuetuNVqntCs6eFOomDcrJaqdOFrXv2kJAxky84MRA/xb7bGlDGAPbTuj1ICputg==", - "dev": true, - "dependencies": { - "@smithy/core": "^1.2.2", - "@smithy/protocol-http": "^3.0.12", - "@smithy/signature-v4": "^2.0.0", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.485.0.tgz", - "integrity": "sha512-3XkFgwVU1XOB33dV7t9BKJ/ptdl2iS+0dxE7ecq8aqT2/gsfKmLCae1G17P8WmdD3z0kMDTvnqM2aWgUnSOkmg==", - "dev": true, - "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/types": "^2.8.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.485.0.tgz", - "integrity": "sha512-cFYF/Bdw7EnT4viSxYpNIv3IBkri/Yb+JpQXl8uDq7bfVJfAN5qZmK07vRkg08xL6TC4F41wshhMSAucGdTwIw==", - "dev": true, - "dependencies": { - "@aws-sdk/credential-provider-env": "3.485.0", - "@aws-sdk/credential-provider-process": "3.485.0", - "@aws-sdk/credential-provider-sso": "3.485.0", - "@aws-sdk/credential-provider-web-identity": "3.485.0", - "@aws-sdk/types": "3.485.0", - "@smithy/credential-provider-imds": "^2.0.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/shared-ini-file-loader": "^2.0.6", - "@smithy/types": "^2.8.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.485.0.tgz", - "integrity": "sha512-2DwzO2azkSzngifKDT61W/DL0tSzewuaFHiLJWdfc8Et3mdAQJ9x3KAj8u7XFpjIcGNqk7FiKjN+zeGUuNiEhA==", - "dev": true, - "dependencies": { - "@aws-sdk/credential-provider-env": "3.485.0", - "@aws-sdk/credential-provider-ini": "3.485.0", - "@aws-sdk/credential-provider-process": "3.485.0", - "@aws-sdk/credential-provider-sso": "3.485.0", - "@aws-sdk/credential-provider-web-identity": "3.485.0", - "@aws-sdk/types": "3.485.0", - "@smithy/credential-provider-imds": "^2.0.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/shared-ini-file-loader": "^2.0.6", - "@smithy/types": "^2.8.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.485.0.tgz", - "integrity": "sha512-X9qS6ZO/rDKYDgWqD1YmSX7sAUUHax9HbXlgGiTTdtfhZvQh1ZmnH6wiPu5WNliafHZFtZT2W07kgrDLPld/Ug==", - "dev": true, - "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/shared-ini-file-loader": "^2.0.6", - "@smithy/types": "^2.8.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.485.0.tgz", - "integrity": "sha512-l0oC8GTrWh+LFQQfSmG1Jai1PX7Mhj9arb/CaS1/tmeZE0hgIXW++tvljYs/Dds4LGXUlaWG+P7BrObf6OyIXA==", - "dev": true, - "dependencies": { - "@aws-sdk/client-sso": "3.485.0", - "@aws-sdk/token-providers": "3.485.0", - "@aws-sdk/types": "3.485.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/shared-ini-file-loader": "^2.0.6", - "@smithy/types": "^2.8.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.485.0.tgz", - "integrity": "sha512-WpBFZFE0iXtnibH5POMEKITj/hR0YV5l2n9p8BEvKjdJ63s3Xke1RN20ZdIyKDaRDwj8adnKDgNPEnAKdS4kLw==", - "dev": true, - "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/types": "^2.8.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.485.0.tgz", - "integrity": "sha512-1mAUX9dQNGo2RIKseVj7SI/D5abQJQ/Os8hQ0NyVAyyVYF+Yjx5PphKgfhM5yoBwuwZUl6q71XPYEGNx7be6SA==", - "dev": true, - "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/protocol-http": "^3.0.12", - "@smithy/types": "^2.8.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-logger": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.485.0.tgz", - "integrity": "sha512-O8IgJ0LHi5wTs5GlpI7nqmmSSagkVdd1shpGgQWY2h0kMSCII8CJZHBG97dlFFpGTvx5EDlhPNek7rl/6F4dRw==", - "dev": true, - "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/types": "^2.8.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.485.0.tgz", - "integrity": "sha512-ZeVNATGNFcqkWDut3luVszROTUzkU5u+rJpB/xmeMoenlDAjPRiHt/ca3WkI5wAnIJ1VSNGpD2sOFLMCH+EWag==", - "dev": true, - "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/protocol-http": "^3.0.12", - "@smithy/types": "^2.8.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-signing": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.485.0.tgz", - "integrity": "sha512-41xzT2p1sOibhsLkdE5rwPJkNbBtKD8Gp36/ySfu0KE415wfXKacElSVxAaBw39/j7iSWDYqqybeEYbAzk+3GQ==", - "dev": true, - "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/property-provider": "^2.0.0", - "@smithy/protocol-http": "^3.0.12", - "@smithy/signature-v4": "^2.0.0", - "@smithy/types": "^2.8.0", - "@smithy/util-middleware": "^2.0.9", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.485.0.tgz", - "integrity": "sha512-CddCVOn+OPQ0CcchketIg+WF6v+MDLAf3GOYTR2htUxxIm7HABuRd6R3kvQ5Jny9CV8gMt22G1UZITsFexSJlQ==", - "dev": true, - "dependencies": { - "@aws-sdk/types": "3.485.0", - "@aws-sdk/util-endpoints": "3.485.0", - "@smithy/protocol-http": "^3.0.12", - "@smithy/types": "^2.8.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.485.0.tgz", - "integrity": "sha512-2FB2EQ0sIE+YgFqGtkE1lDIMIL6nYe6MkOHBwBM7bommadKIrbbr2L22bPZGs3ReTsxiJabjzxbuCAVhrpHmhg==", - "dev": true, - "dependencies": { - "@smithy/node-config-provider": "^2.1.9", - "@smithy/types": "^2.8.0", - "@smithy/util-config-provider": "^2.1.0", - "@smithy/util-middleware": "^2.0.9", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/token-providers": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.485.0.tgz", - "integrity": "sha512-kOXA1WKIVIFNRqHL8ynVZ3hCKLsgnEmGr2iDR6agDNw5fYIlCO/6N2xR6QdGcLTvUUbwOlz4OvKLUQnWMKAnnA==", - "dev": true, - "dependencies": { - "@aws-crypto/sha256-browser": "3.0.0", - "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/middleware-host-header": "3.485.0", - "@aws-sdk/middleware-logger": "3.485.0", - "@aws-sdk/middleware-recursion-detection": "3.485.0", - "@aws-sdk/middleware-user-agent": "3.485.0", - "@aws-sdk/region-config-resolver": "3.485.0", - "@aws-sdk/types": "3.485.0", - "@aws-sdk/util-endpoints": "3.485.0", - "@aws-sdk/util-user-agent-browser": "3.485.0", - "@aws-sdk/util-user-agent-node": "3.485.0", - "@smithy/config-resolver": "^2.0.23", - "@smithy/fetch-http-handler": "^2.3.2", - "@smithy/hash-node": "^2.0.18", - "@smithy/invalid-dependency": "^2.0.16", - "@smithy/middleware-content-length": "^2.0.18", - "@smithy/middleware-endpoint": "^2.3.0", - "@smithy/middleware-retry": "^2.0.26", - "@smithy/middleware-serde": "^2.0.16", - "@smithy/middleware-stack": "^2.0.10", - "@smithy/node-config-provider": "^2.1.9", - "@smithy/node-http-handler": "^2.2.2", - "@smithy/property-provider": "^2.0.0", - "@smithy/protocol-http": "^3.0.12", - "@smithy/shared-ini-file-loader": "^2.0.6", - "@smithy/smithy-client": "^2.2.1", - "@smithy/types": "^2.8.0", - "@smithy/url-parser": "^2.0.16", - "@smithy/util-base64": "^2.0.1", - "@smithy/util-body-length-browser": "^2.0.1", - "@smithy/util-body-length-node": "^2.1.0", - "@smithy/util-defaults-mode-browser": "^2.0.24", - "@smithy/util-defaults-mode-node": "^2.0.32", - "@smithy/util-endpoints": "^1.0.8", - "@smithy/util-retry": "^2.0.9", - "@smithy/util-utf8": "^2.0.2", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/types": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.485.0.tgz", - "integrity": "sha512-+QW32YQdvZRDOwrAQPo/qCyXoSjgXB6RwJwCwkd8ebJXRXw6tmGKIHaZqYHt/LtBymvnaBgBBADNa4+qFvlOFw==", - "dev": true, - "dependencies": { - "@smithy/types": "^2.8.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-endpoints": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.485.0.tgz", - "integrity": "sha512-dTd642F7nJisApF8YjniqQ6U59CP/DCtar11fXf1nG9YNBCBsNNVw5ZfZb5nSNzaIdy27mQioWTCV18JEj1mxg==", - "dev": true, - "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/util-endpoints": "^1.0.8", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.485.0.tgz", - "integrity": "sha512-QliWbjg0uOhGTcWgWTKPMY0SBi07g253DjwrCINT1auqDrdQPxa10xozpZExBYjAK2KuhYDNUzni127ae6MHOw==", - "dev": true, - "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/types": "^2.8.0", - "bowser": "^2.11.0", - "tslib": "^2.5.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.485.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.485.0.tgz", - "integrity": "sha512-QF+aQ9jnDlPUlFBxBRqOylPf86xQuD3aEPpOErR+50qJawVvKa94uiAFdvtI9jv6hnRZmuFsTj2rsyytnbAYBA==", - "dev": true, - "dependencies": { - "@aws-sdk/types": "3.485.0", - "@smithy/node-config-provider": "^2.1.9", - "@smithy/types": "^2.8.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, "node_modules/@aws-sdk/client-dynamodb": { "version": "3.525.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.525.0.tgz", From abc7c3f159abdc78a6d99ccdc55fb48ba5223984 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:34:18 +0100 Subject: [PATCH 033/103] chore(deps-dev): bump @typescript-eslint/parser from 6.19.0 to 6.19.1 (#1946) Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.19.0 to 6.19.1. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.19.1/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/parser" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 75 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/package-lock.json b/package-lock.json index b9a5b8edda..56f1fa4333 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5178,6 +5178,81 @@ } } }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz", + "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.19.1", + "@typescript-eslint/visitor-keys": "6.19.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz", + "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz", + "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.19.1", + "@typescript-eslint/visitor-keys": "6.19.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", + "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.19.1", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", From 330d9f9e7651bb89b0c9a84a59bba81b48fc4f77 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:39:34 +0100 Subject: [PATCH 034/103] chore(deps-dev): bump @typescript-eslint/eslint-plugin (#1948) Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.19.0 to 6.19.1. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.19.1/packages/eslint-plugin) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 58 ----------------------------------------------- 1 file changed, 58 deletions(-) diff --git a/package-lock.json b/package-lock.json index 56f1fa4333..fd99e51e40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5178,64 +5178,6 @@ } } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz", - "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz", - "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz", - "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { "version": "6.19.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", From 330a65ced81f23f43537cac956422a18d5bca2e9 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 26 Jan 2024 17:19:52 +0100 Subject: [PATCH 035/103] chore(ci): refactor workflows to scope permissions (#1978) --- .github/workflows/reusable_publish_docs.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/reusable_publish_docs.yml b/.github/workflows/reusable_publish_docs.yml index 2659a92ba9..3b8fc275d2 100644 --- a/.github/workflows/reusable_publish_docs.yml +++ b/.github/workflows/reusable_publish_docs.yml @@ -65,10 +65,6 @@ jobs: - name: Install doc generation dependencies run: | pip install --require-hashes -r docs/requirements.txt - - name: Setup doc deploy - run: | - git config --global user.name Docs deploy - git config --global user.email aws-devax-open-source@amazon.com - name: Git refresh tip (detached mode) # Git Detached mode (release notes) doesn't have origin if: ${{ inputs.detached_mode }} From 96888ec54662ad2c44c23544ebd6aac5e0ec9f2b Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sat, 27 Jan 2024 13:52:48 +0100 Subject: [PATCH 036/103] chore(maintenance): rebase conflicts --- packages/logger/src/Logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index 810c1a8613..3a639aeaf2 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -432,7 +432,7 @@ class Logger extends Utility implements LoggerInterface { logger.addContext(context); let shouldLogEvent = undefined; - if (Object.hasOwn(options || {}, 'logEvent')) { + if (options && options.hasOwnProperty('logEvent')) { shouldLogEvent = options!.logEvent; } logger.logEventIfEnabled(event, shouldLogEvent); From 013e230d113361fcf714982ca8ed458415eece63 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sat, 27 Jan 2024 14:03:06 +0100 Subject: [PATCH 037/103] chore(maintenance): rebase conflicts --- .github/ISSUE_TEMPLATE/bug_report.yml | 12 ++++++------ .github/dependabot.yml | 2 ++ .../reusable-run-linting-check-and-unit-tests.yml | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3b9c913987..06af982ec3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,14 +1,14 @@ name: Bug report description: Report a reproducible bug to help us improve -title: 'Bug: TITLE' -labels: ['type/bug', 'triage'] -projects: ['aws-powertools/7'] +title: "Bug: TITLE" +labels: ["type/bug", "triage"] +projects: ["aws-powertools/7"] body: - type: markdown attributes: value: | Thank you for submitting a bug report. Before you start, make sure that [the bug hasn't been reported already](https://github.com/aws-powertools/powertools-lambda-typescript/issues). - + Please add as much information as possible to help us reproduce, and remove any potential sensitive data. - type: textarea id: expected_behaviour @@ -58,7 +58,7 @@ body: id: version attributes: label: Powertools for AWS Lambda (TypeScript) version - placeholder: 'latest, 2.0.0' + placeholder: "latest, 1.3.0" value: latest validations: required: true @@ -95,4 +95,4 @@ body: value: | --- - **Disclaimer**: After creating an issue, please wait until it is triaged and confirmed by a maintainer before implementing it. This will reduce amount of rework and the chance that a pull request gets rejected. + **Disclaimer**: After creating an issue, please wait until it is triaged and confirmed by a maintainer before implementing it. This will reduce amount of rework and the chance that a pull request gets rejected. \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fd4d533082..b83c97aea3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -40,9 +40,11 @@ updates: aws-sdk: patterns: - "@aws-sdk/**" + - "@smithy/**" - "aws-sdk-client-mock" - "aws-sdk-client-mock-jest" aws-cdk: patterns: - "@aws-cdk/**" - "aws-cdk-lib" + - "aws-cdk" diff --git a/.github/workflows/reusable-run-linting-check-and-unit-tests.yml b/.github/workflows/reusable-run-linting-check-and-unit-tests.yml index 4398d4a8be..d7d7b2bd0a 100644 --- a/.github/workflows/reusable-run-linting-check-and-unit-tests.yml +++ b/.github/workflows/reusable-run-linting-check-and-unit-tests.yml @@ -22,7 +22,7 @@ jobs: uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: ${{ matrix.version }} - cache: 'npm' + cache: "npm" - name: Setup dependencies uses: ./.github/actions/cached-node-modules with: @@ -37,7 +37,7 @@ jobs: NODE_ENV: dev strategy: matrix: - example: ['sam', 'cdk'] + example: ["sam", "cdk"] fail-fast: false defaults: run: From 0f6191d3e2b4afd97fb9a7ed3883c7839634817d Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 20 Feb 2024 20:50:39 +0100 Subject: [PATCH 038/103] chore: remove v2 specific release --- .github/scripts/release_patch_package_json.js | 10 +--------- v2.json | 3 --- 2 files changed, 1 insertion(+), 12 deletions(-) delete mode 100644 v2.json diff --git a/.github/scripts/release_patch_package_json.js b/.github/scripts/release_patch_package_json.js index 932bc7b109..509bea003c 100644 --- a/.github/scripts/release_patch_package_json.js +++ b/.github/scripts/release_patch_package_json.js @@ -17,15 +17,7 @@ if (process.argv.length < 3) { } const basePath = resolve(process.argv[2]); const packageJsonPath = join(basePath, 'package.json'); -const alphaPackages = [ - '@aws-lambda-powertools/batch', - '@aws-lambda-powertools/commons', - '@aws-lambda-powertools/idempotency', - '@aws-lambda-powertools/logger', - '@aws-lambda-powertools/metrics', - '@aws-lambda-powertools/parameters', - '@aws-lambda-powertools/tracer', -]; +const alphaPackages = []; const betaPackages = []; (() => { diff --git a/v2.json b/v2.json deleted file mode 100644 index f732091fe5..0000000000 --- a/v2.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "iteration": 0 -} \ No newline at end of file From 309fc4da1552a868f8c750b904e9d7cc8be0d328 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 20 Feb 2024 20:55:59 +0100 Subject: [PATCH 039/103] chore: release patch script --- .github/scripts/release_patch_package_json.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/scripts/release_patch_package_json.js b/.github/scripts/release_patch_package_json.js index 509bea003c..761c0680a1 100644 --- a/.github/scripts/release_patch_package_json.js +++ b/.github/scripts/release_patch_package_json.js @@ -50,16 +50,7 @@ const betaPackages = []; let version = originalVersion; // If the package is an alpha or beta package, update the version number to include a suffix if (alphaPackages.includes(name)) { - const iteration = JSON.parse( - readFileSync(resolve('..', '..', 'v2.json'), 'utf8') - ).iteration; - version = `${version}-alpha.${iteration}`; - dependencies && - Object.entries(dependencies).forEach(([dependencyName, version]) => { - if (alphaPackages.includes(dependencyName)) { - dependencies[dependencyName] = `${version}-alpha.${iteration}`; - } - }); + version = `${version}-alpha`; } else if (betaPackages.includes(name)) { version = `${version}-beta`; } From a31bc2567997f30b89085a6a26433662ac7750af Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 20 Feb 2024 22:14:57 +0100 Subject: [PATCH 040/103] chore: address SonarCloud findings --- layers/tests/e2e/layerPublisher.class.test.functionCode.ts | 7 ++----- packages/logger/src/Logger.ts | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/layers/tests/e2e/layerPublisher.class.test.functionCode.ts b/layers/tests/e2e/layerPublisher.class.test.functionCode.ts index 3f15dfbbb1..b95eeafea9 100644 --- a/layers/tests/e2e/layerPublisher.class.test.functionCode.ts +++ b/layers/tests/e2e/layerPublisher.class.test.functionCode.ts @@ -91,12 +91,9 @@ export const handler = async (): Promise => { 'batch', ]) { const moduleVersion = await getVersionFromModule(moduleName); - // TODO: remove this check once v2 becomes GA - // if (moduleVersion != expectedVersion) { - if (!moduleVersion.startsWith(expectedVersion)) { + if (moduleVersion != expectedVersion) { throw new Error( - // `Package version mismatch (${moduleName}): ${moduleVersion} != ${expectedVersion}` - `Package version mismatch (${moduleName}): ${moduleVersion} does not start with ${expectedVersion}` + `Package version mismatch (${moduleName}): ${moduleVersion} != ${expectedVersion}` ); } } diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index 3a639aeaf2..d454d50a09 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -433,7 +433,7 @@ class Logger extends Utility implements LoggerInterface { let shouldLogEvent = undefined; if (options && options.hasOwnProperty('logEvent')) { - shouldLogEvent = options!.logEvent; + shouldLogEvent = options.logEvent; } logger.logEventIfEnabled(event, shouldLogEvent); } From bf0f57c3a2168b737b4a39e1a6a45732305b8284 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 22 Feb 2024 15:41:33 +0100 Subject: [PATCH 041/103] chore: remove unused dependency --- package-lock.json | 17 ----------------- package.json | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index fd99e51e40..b9a5b8edda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5178,23 +5178,6 @@ } } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", - "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.19.1", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/@typescript-eslint/scope-manager": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", diff --git a/package.json b/package.json index e30fa7c00f..c1549c894e 100644 --- a/package.json +++ b/package.json @@ -74,4 +74,4 @@ "engines": { "node": ">=16" } -} \ No newline at end of file +} From 038b9267a5c166cb7e8ddbbd5457e47bc4297788 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sat, 3 Jun 2023 23:38:32 +0000 Subject: [PATCH 042/103] wip: jmespath util --- package-lock.json | 14 +- package.json | 3 +- packages/jmespath/jest.config.js | 28 + packages/jmespath/package.json | 50 ++ packages/jmespath/src/Lexer.ts | 320 +++++++++ packages/jmespath/src/ParsedResult.ts | 31 + packages/jmespath/src/Parser.ts | 185 +++++ packages/jmespath/src/ast.ts | 313 +++++++++ packages/jmespath/src/compile.ts | 23 + packages/jmespath/src/constants.ts | 68 ++ packages/jmespath/src/errors.ts | 181 +++++ packages/jmespath/src/index.ts | 1 + packages/jmespath/src/search.ts | 20 + .../jmespath/src/visitor/GraphvizVisitor.ts | 14 + .../jmespath/src/visitor/TreeInterpreter.ts | 22 + packages/jmespath/src/visitor/index.ts | 2 + .../helpers/populateEnvironmentVariables.ts | 1 + packages/jmespath/tests/unit/base.test.ts | 75 ++ packages/jmespath/tests/unit/current.tests.ts | 0 packages/jmespath/tests/unit/unicode.test.ts | 80 +++ packages/jmespath/tests/unit/wildcard.test.ts | 662 ++++++++++++++++++ packages/jmespath/tsconfig.json | 39 ++ 22 files changed, 2129 insertions(+), 3 deletions(-) create mode 100644 packages/jmespath/jest.config.js create mode 100644 packages/jmespath/package.json create mode 100644 packages/jmespath/src/Lexer.ts create mode 100644 packages/jmespath/src/ParsedResult.ts create mode 100644 packages/jmespath/src/Parser.ts create mode 100644 packages/jmespath/src/ast.ts create mode 100644 packages/jmespath/src/compile.ts create mode 100644 packages/jmespath/src/constants.ts create mode 100644 packages/jmespath/src/errors.ts create mode 100644 packages/jmespath/src/index.ts create mode 100644 packages/jmespath/src/search.ts create mode 100644 packages/jmespath/src/visitor/GraphvizVisitor.ts create mode 100644 packages/jmespath/src/visitor/TreeInterpreter.ts create mode 100644 packages/jmespath/src/visitor/index.ts create mode 100644 packages/jmespath/tests/helpers/populateEnvironmentVariables.ts create mode 100644 packages/jmespath/tests/unit/base.test.ts create mode 100644 packages/jmespath/tests/unit/current.tests.ts create mode 100644 packages/jmespath/tests/unit/unicode.test.ts create mode 100644 packages/jmespath/tests/unit/wildcard.test.ts create mode 100644 packages/jmespath/tsconfig.json diff --git a/package-lock.json b/package-lock.json index b9a5b8edda..fa8a71ebcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,8 @@ "docs/snippets", "layers", "examples/cdk", - "examples/sam" + "examples/sam", + "packages/jmespath" ], "devDependencies": { "@middy/core": "^4.7.0", @@ -292,6 +293,10 @@ "resolved": "packages/idempotency", "link": true }, + "node_modules/@aws-lambda-powertools/jmespath": { + "resolved": "packages/jmespath", + "link": true + }, "node_modules/@aws-lambda-powertools/logger": { "resolved": "packages/logger", "link": true @@ -17202,6 +17207,11 @@ } } }, + "packages/jmespath": { + "version": "1.8.0-alpha.0", + "license": "MIT-0", + "devDependencies": {} + }, "packages/logger": { "name": "@aws-lambda-powertools/logger", "version": "2.0.2", @@ -17331,4 +17341,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index c1549c894e..98ff27db95 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "docs/snippets", "layers", "examples/cdk", - "examples/sam" + "examples/sam", + "packages/jmespath" ], "scripts": { "test": "npm t -ws", diff --git a/packages/jmespath/jest.config.js b/packages/jmespath/jest.config.js new file mode 100644 index 0000000000..5dbf28b722 --- /dev/null +++ b/packages/jmespath/jest.config.js @@ -0,0 +1,28 @@ +module.exports = { + displayName: { + name: 'Powertools for AWS Lambda (TypeScript) utility: JMESPATH', + color: 'purple', + }, + runner: 'groups', + preset: 'ts-jest', + transform: { + '^.+\\.ts?$': 'ts-jest', + }, + moduleFileExtensions: ['js', 'ts'], + collectCoverageFrom: ['**/src/**/*.ts', '!**/node_modules/**'], + testMatch: ['**/?(*.)+(spec|test).ts'], + roots: ['/src', '/tests'], + testPathIgnorePatterns: ['/node_modules/'], + testEnvironment: 'node', + coveragePathIgnorePatterns: ['/node_modules/', '/types/'], + coverageThreshold: { + global: { + statements: 100, + branches: 100, + functions: 100, + lines: 100, + }, + }, + coverageReporters: ['json-summary', 'text', 'lcov'], + setupFiles: ['/tests/helpers/populateEnvironmentVariables.ts'], +}; diff --git a/packages/jmespath/package.json b/packages/jmespath/package.json new file mode 100644 index 0000000000..58e45b44e6 --- /dev/null +++ b/packages/jmespath/package.json @@ -0,0 +1,50 @@ +{ + "name": "@aws-lambda-powertools/jmespath", + "version": "1.8.0-alpha.0", + "description": "The jmespath package for the Powertools for AWS Lambda (TypeScript) library", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "npm run test:unit", + "test:unit": "jest --group=unit --detectOpenHandles --coverage --verbose", + "test:e2e:nodejs14x": "RUNTIME=nodejs14x jest --group=e2e", + "test:e2e:nodejs16x": "RUNTIME=nodejs16x jest --group=e2e", + "test:e2e:nodejs18x": "RUNTIME=nodejs18x jest --group=e2e", + "test:e2e": "jest --group=e2e", + "build": "tsc", + "lint": "eslint --ext .ts,.js --no-error-on-unmatched-pattern .", + "lint-fix": "eslint --fix --ext .ts,.js --no-error-on-unmatched-pattern ." + }, + "lint-staged": { + "*.ts": "npm run lint-fix", + "*.js": "npm run lint-fix" + }, + "homepage": "https://github.com/awslabs/aws-lambda-powertools-typescript/tree/main/packages/jmespath#readme", + "license": "MIT-0", + "main": "./lib/index.js", + "types": "./lib/index.d.ts", + "files": [ + "lib" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/awslabs/aws-lambda-powertools-typescript.git" + }, + "bugs": { + "url": "https://github.com/awslabs/aws-lambda-powertools-typescript/issues" + }, + "keywords": [ + "aws", + "lambda", + "powertools", + "jmespath", + "functions", + "serverless", + "nodejs" + ] +} \ No newline at end of file diff --git a/packages/jmespath/src/Lexer.ts b/packages/jmespath/src/Lexer.ts new file mode 100644 index 0000000000..867fde4efb --- /dev/null +++ b/packages/jmespath/src/Lexer.ts @@ -0,0 +1,320 @@ +import { + START_IDENTIFIER, + VALID_IDENTIFIER, + VALID_NUMBER, + WHITESPACE, + SIMPLE_TOKENS, +} from './constants'; +import { LexerError, EmptyExpressionError } from './errors'; + +/** + * TODO: write docs for Token type & extract to own file. + */ +type Token = { + type: string; + value: string | number; + start: number; + end: number; +}; + +class Lexer { + #position!: number; + #expression!: string; + #chars!: string[]; + #current!: string; + #length!: number; + + public *tokenize(expression: string): Generator { + this.#initializeForExpression(expression); + while (this.#current !== '') { + if (SIMPLE_TOKENS.has(this.#current)) { + yield { + // We know that SIMPLE_TOKENS has this.#current as a key because + // we checked for that above. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + type: SIMPLE_TOKENS.get(this.#current)!, + value: this.#current, + start: this.#position, + end: this.#position + 1, + }; + + this.#next(); + } else if (START_IDENTIFIER.has(this.#current)) { + const start = this.#position; + let buff = this.#current; + while (VALID_IDENTIFIER.has(this.#next())) { + buff += this.#current; + } + yield { + type: 'unquoted_identifier', + value: buff, + start, + end: start + buff.length, + }; + } else if (WHITESPACE.has(this.#current)) { + this.#next(); + } else if (this.#current === '[') { + const start = this.#position; + const nextChar = this.#next(); + if (nextChar == ']') { + this.#next(); + yield { type: 'flatten', value: '[]', start: start, end: start + 2 }; + } else if (nextChar == '?') { + this.#next(); + yield { type: 'filter', value: '[?', start: start, end: start + 2 }; + } else { + yield { type: 'lbracket', value: '[', start: start, end: start + 1 }; + } + } else if (this.#current === `'`) { + yield this.#consumeRawStringLiteral(); + } else if (this.#current === '|') { + yield this.#matchOrElse('|', 'or', 'pipe'); + } else if (this.#current === '&') { + yield this.#matchOrElse('&', 'and', 'expref'); + } else if (this.#current === '`') { + yield this.#consumeLiteral(); + } else if (VALID_NUMBER.has(this.#current)) { + const start = this.#position; + const buff = this.#consumeNumber(); + yield { + type: 'number', + value: parseInt(buff), + start: start, + end: start + buff.length, + }; + } else if (this.#current === '-') { + // Negative number. + const start = this.#position; + const buff = this.#consumeNumber(); + if (buff.length > 1) { + yield { + type: 'number', + value: parseInt(buff), + start: start, + end: start + buff.length, + }; + } else { + // TODO: see if we can pass the error message `"Unknown token '%s'" % buff` to the LexerError + throw new LexerError(start, buff); + } + } else if (this.#current === '"') { + yield this.#consumeQuotedIdentifier(); + } else if (this.#current === '<') { + yield this.#matchOrElse('=', 'lte', 'lt'); + } else if (this.#current === '>') { + yield this.#matchOrElse('=', 'gte', 'gt'); + } else if (this.#current === '!') { + yield this.#matchOrElse('=', 'ne', 'not'); + } else if (this.#current === '=') { + if (this.#next() === '=') { + yield { + type: 'eq', + value: '==', + start: this.#position - 1, + end: this.#position, + }; + this.#next(); + } else { + let position; + // TODO: check this this.#current === undefined case + if (this.#current === undefined) { + // If we're at the EOF, we never advanced + // the position so we don't need to rewind + // it back one location. + position = this.#position; + } else { + position = this.#position - 1; + } + // TODO: see if we can pass a message "Unknown token '='" to LexerError + throw new LexerError(position, '='); + } + } else { + // TODO: see if we can pass a message `Unknown token ${this.#current}` to LexerError + throw new LexerError(this.#position, this.#current); + } + yield { type: 'eof', value: '', start: this.#length, end: this.#length }; + } + } + + /** + * TODO: write docs for Lexer.#consumeNumber() + * TODO: finalize types for Lexer.#consumeNumber() + */ + #consumeNumber(): string { + let buff = this.#current; + while (this.#next() in VALID_NUMBER) { + buff += this.#current; + } + + return buff; + } + + /** + * Initializes the lexer for the given expression. + * + * We use a separate method for this instead of the constructor + * because we want to be able to reuse the same lexer instance + * and also because we want to be able to expose a public API + * for tokenizing expressions like `new Lexer().tokenize(expression)`. + * + * @param expression The JMESPath expression to tokenize. + */ + #initializeForExpression(expression: string): void { + if (typeof expression !== 'string') { + throw new EmptyExpressionError(); + } + + this.#position = 0; + this.#expression = expression; + this.#chars = Array.from(expression); + this.#current = this.#chars[0]; + this.#length = this.#expression.length; + } + + /** + * Advance the lexer to the next character in the expression. + * + * @returns The next character in the expression. + */ + #next(): string { + if (this.#position === this.#length - 1) { + this.#current = ''; + } else { + this.#position += 1; + this.#current = this.#chars[this.#position]; + } + + return this.#current; + } + + /** + * Consume until the given delimiter is reached allowing + * for escaping of the delimiter with a backslash (`\`). + * + * @param delimiter The delimiter to consume until. + */ + #consumeUntil(delimiter: string): string { + const start = this.#position; + let buff = ''; + this.#next(); + while (this.#current !== delimiter) { + if (this.#current === '\\') { + buff += '\\'; + this.#next(); + } + if (this.#current === '') { + // We've reached the end of the expression (EOF) before + // we found the delimiter. This is an error. + // TODO: see if we can pass a message to the Lexer.#consumeUntil() call + // @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/lexer.py#L151 + throw new LexerError(start, this.#expression.substring(start)); + } + buff += this.#current; + this.#next(); + } + // Skip the closing delimiter + this.#next(); + + return buff; + } + + /** + * TODO: write docs for Lexer.#consumeLiteral() + * + * @returns + */ + #consumeLiteral(): Token { + const start = this.#position; + const lexeme = this.#consumeUntil('`').replace('\\`', '`'); + try { + const parsedJson = JSON.parse(lexeme); + + return { + type: 'literal', + value: parsedJson, + start, + end: this.#position - start, + }; + } catch (error) { + // TODO: see if we can get the error message from JSON.parse() and use that + // @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/lexer.py#L174 + throw new LexerError(start, lexeme); + } + } + + /** + * TODO: write docs for Lexer.#consumeQuotedIdentifier() + * + * @returns + */ + #consumeQuotedIdentifier(): Token { + const start = this.#position; + const lexeme = '"' + this.#consumeUntil('"') + '"'; + try { + const tokenLen = this.#position - start; + + return { + type: 'quoted_identifier', + value: JSON.parse(lexeme), + start, + end: tokenLen, + }; + } catch (error) { + // TODO: see if we can get the error message from JSON.parse() and use that + // @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/lexer.py#L187 + // const errorMessage = `Invalid quoted identifier: ${lexeme}`; + throw new LexerError(start, lexeme); + } + } + + /** + * TODO: write docs for Lexer.#consumeRawStringLiteral() + * + * @returns + */ + #consumeRawStringLiteral(): Token { + const start = this.#position; + const lexeme = this.#consumeUntil('"').replace(`\\'`, `'`); + const tokenLen = this.#position - start; + + return { + type: 'literal', + value: lexeme, + start, + end: tokenLen, + }; + } + + /** + * TODO: write docs for Lexer.#matchOrElse() + * + * @param expected + * @param matchType + * @param elseType + * @returns + */ + #matchOrElse(expected: string, matchType: string, elseType: string): Token { + const start = this.#position; + const current = this.#current; + const nextChar = this.#next(); + if (nextChar === expected) { + this.#next(); + + return { + type: matchType, + value: current + nextChar, + start, + end: start + 2, + }; + } + + return { + type: elseType, + value: current, + start, + end: start, + }; + } +} + +export { Lexer }; diff --git a/packages/jmespath/src/ParsedResult.ts b/packages/jmespath/src/ParsedResult.ts new file mode 100644 index 0000000000..7858d8afd1 --- /dev/null +++ b/packages/jmespath/src/ParsedResult.ts @@ -0,0 +1,31 @@ +import { TreeInterpreter, GraphvizVisitor } from './visitor'; + +class ParsedResult { + public expression: string; + public parsed: unknown[]; + + public constructor(expression: string, parsed: unknown[]) { + this.expression = expression; + this.parsed = parsed; + } + + /** + * Render the parsed AST as a dot file. + * + * TODO: write docs for ParsedResult#renderDotFile() + * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/parser.py#L515-L519 + */ + public renderDotFile(): string { + const renderer = new GraphvizVisitor(); + + return renderer.visit(this.parsed); + } + + public search(value: unknown, options?: unknown): unknown { + const interpreter = new TreeInterpreter(options); + + return interpreter.visit(this.parsed, value); + } +} + +export { ParsedResult }; diff --git a/packages/jmespath/src/Parser.ts b/packages/jmespath/src/Parser.ts new file mode 100644 index 0000000000..6140d61b27 --- /dev/null +++ b/packages/jmespath/src/Parser.ts @@ -0,0 +1,185 @@ +import { BINDING_POWER } from './constants'; +import { field, literal } from './ast'; +import { Lexer } from './Lexer'; +import { ParsedResult } from './ParsedResult'; +import { LexerError, IncompleteExpressionError, ParseError } from './errors'; +/** + * Top down operaotr precedence parser for JMESPath. + * + * ## References + * The implementation of this Parser is based on the implementation of + * [jmespath.py](https://github.com/jmespath/jmespath.py/), which in turn + * is based on [Vaughan R. Pratt's "Top Down Operator Precedence"](http://dl.acm.org/citation.cfm?doid=512927.512931). + * + * If you don't want to read the full paper, there are some other good + * overviews that explain the general idea: + * - [Pratt Parsers: Expression Parsing Made Easy](https://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/) + * - [Simple Top-Down Parsing in Python](http://effbot.org/zone/simple-top-down-parsing.htm) + * - [Top Down Operator Precedence](http://javascript.crockford.com/tdop/tdop.html) + */ +class Parser { + #bindingPowers: typeof BINDING_POWER = BINDING_POWER; + /** + * The maximum binding power for a token + * that can stop a projection. + */ + #projectionStop = 10; + /** + * Cache object + */ + #cache: Record = {}; + /** + * The maximum size of the cache. + */ + #maxCacheSize = 128; + #tokenizer?: Lexer; + #tokens: unknown[]; + #bufferSize: number; + #index = 0; + + public constructor(lookahead = 2) { + this.#tokens = Array.from({ length: lookahead }); + this.#bufferSize = lookahead; + } + + /** + * TODO: write docs for Parser.parse() + * + * @param expression The JMESPath expression to parse. + * @returns The parsed expression. + */ + public parse(expression: string): ParsedResult { + const cached = this.#cache[expression]; + if (cached) { + return cached; + } + const parsedResult = this.#doParse(expression); + this.#cache[expression] = parsedResult; + if (Object.keys(this.#cache).length > this.#maxCacheSize) { + this.#evictCache(); + } + + return parsedResult; + } + + /** + * TODO: write docs for Parser.#doParse() + * + * @param expression The JMESPath expression to parse. + * @returns The parsed expression. + */ + #doParse(expression: string): ParsedResult { + try { + return this.#parse(expression); + } catch (error) { + if (error instanceof LexerError) { + error.expression = expression; + throw error; + } else if (error instanceof IncompleteExpressionError) { + error.expression = expression; + throw error; + } else if (error instanceof ParseError) { + error.expression = expression; + throw error; + } else { + throw error; + } + } + } + + /** + * TODO: write docs for Parser.#parse() + */ + #parse(expression: string): ParsedResult { + this.#tokenizer = new Lexer(); + this.#tokens = this.#tokenizer.tokenize(expression); + this.#index = 0; + const parsed = this.#expression(0); + if (this.#currentToken() !== 'eof') { + const token = this.#lookaheadToken(0); + throw new ParseError({ + lexPosition: token.start, + tokenValue: token.value, + tokenType: token.type, + }); + } + + return new ParsedResult(expression, parsed); + } + + /** + * TODO: write docs for Parser.#expression() + */ + #expression(bindingPower: number): unknown { + const leftToken = this.#lookaheadToken(0); + this.#advance(); + const nudFunction = this.#getNudFunction(leftToken.type); + let left = nudFunction(leftToken); + let currentToken = this.#currentToken(); + while (bindingPower < this.#bindingPowers[currentToken]) { + const ledFunction = this.#getLedFunction(currentToken); + this.#advance(); + left = ledFunction(left); + currentToken = this.#currentToken(); + } + + return left; + } + + /** + * TODO: write docs for arser.#advance() + * TODO: complete `Parser.#getNudFunction()` implementation using `ast.tokenType` + * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/parser.py#L121-L123 + * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/parser.py#L137-L138 + * + * @param tokenType The type of token to get the nud function for. + * @returns not sure + */ + #getNudFunction(token: unknown): unknown { + if (token.type === 'literal') { + return literal(token.value); + } else if (token.type === 'unquoted_identifier') { + return field(token.value); + } else if (token.type === 'quoted_identifier') { + const fieldValue = field(token.value); + // You can't have a quoted identifier as a function name + if (this.#currentToken() === 'lparen') { + const token = this.#lookaheadToken(0); + throw new ParseError({ + lexPosition: 0, + tokenValue: token.value, + tokenType: token.type, + reason: 'Quoted identifier cannot be used as a function name', + }); + } + + return fieldValue; + } else if () + else { + if (token.type === 'eof') { + throw new ParseError({ + lexPosition: token.start, + tokenValue: token.value, + tokenType: token.type, + reason: 'invalid token', + }); + } + } + } + + #ledFunction(token: unknown): unknown { + const method = `#led${token.type}`; + if (!method) { + throw new ParseError({ + lexPosition: token.start, + tokenValue: token.value, + tokenType: token.type, + reason: 'invalid token', + }); + } + + return this[method]; + } +} + +export { Parser }; diff --git a/packages/jmespath/src/ast.ts b/packages/jmespath/src/ast.ts new file mode 100644 index 0000000000..79a6a2f94c --- /dev/null +++ b/packages/jmespath/src/ast.ts @@ -0,0 +1,313 @@ +/** + * TODO: finalize ASTThing type & extract + */ +type ASTThing = { + type: string; + children: unknown[]; + value?: unknown; +}; + +/** + * TODO: write docs for comparator() + * TODO: finalize types for comparator() + */ +const comparator = ( + name: unknown, + first: unknown, + second: unknown +): ASTThing => ({ + type: 'comparator', + children: [first, second], + value: name, +}); + +/** + * TODO: write docs for currentNode() + * TODO: finalize types for currentNode() + */ +const currentNode = (): ASTThing => ({ + type: 'current', + children: [], +}); + +/** + * TODO: write docs for expref() + * TODO: finalize types for expref() + */ +const expref = (expression: unknown): ASTThing => ({ + type: 'expref', + children: [expression], +}); + +/** + * TODO: write docs for functionExpression() + * TODO: finalize types for functionExpression() + */ +const functionExpression = (name: unknown, args: unknown[]): ASTThing => ({ + type: 'function_expression', + children: args, + value: name, +}); + +/** + * TODO: write docs for field() + * TODO: finalize types for field() + */ +const field = (name: unknown): ASTThing => ({ + type: 'field', + children: [], + value: name, +}); + +/** + * TODO: write docs for fieldExpression() + * TODO: finalize types for fieldExpression() + * + * @param left + * @param right + * @param comparator + * @returns + */ +const filterProjection = ( + left: unknown, + right: unknown, + comparator: unknown +): ASTThing => ({ + type: 'filter_projection', + children: [left, right, comparator], +}); + +/** + * TODO: write docs for flatten() + * TODO: finalize types for flatten() + * + * @param left + * @param right + * @param comparator + * @returns + */ +const flatten = (node: unknown): ASTThing => ({ + type: 'flatten', + children: [node], +}); + +/** + * TODO: write docs for identity() + * TODO: finalize types for identity() + * + * @param left + * @param right + * @param comparator + * @returns + */ +const identity = (): ASTThing => ({ type: 'identity', children: [] }); + +/** + * TODO: write docs for index() + * TODO: finalize types for index() + * + * @param left + * @param right + * @param comparator + * @returns + */ +const index = (index: unknown): ASTThing => ({ + type: 'index', + value: index, + children: [], +}); + +/** + * TODO: write docs for indexExpression() + * TODO: finalize types for indexExpression() + * + * @param left + * @param right + * @param comparator + * @returns + */ +const indexExpression = (children: unknown[]): ASTThing => ({ + type: 'index_expression', + children: children, +}); + +/** + * TODO: write docs for keyValPair() + * TODO: finalize types for keyValPair() + * + * @param left + * @param right + * @param comparator + * @returns + */ +const keyValPair = (keyName: string, node: unknown): ASTThing => ({ + type: 'key_val_pair', + children: [node], + value: keyName, +}); + +/** + * TODO: write docs for literal() + * TODO: finalize types for literal() + * + * @param left + * @param right + * @param comparator + * @returns + */ +const literal = (literalValue: unknown): ASTThing => ({ + type: 'literal', + value: literalValue, + children: [], +}); + +/** + * TODO: write docs for multiSelectDict() + * TODO: finalize types for multiSelectDict() + * TODO: check if multiSelectDict() could be possibly be renamed to multiSelectObject() / multiSelectMap() / multiSelectHash() + * + * @param left + * @param right + * @param comparator + * @returns + */ +const multiSelectDict = (nodes: unknown[]): ASTThing => ({ + type: 'multi_select_dict', + children: nodes, +}); + +/** + * TODO: write docs for multiSelectList() + * TODO: finalize types for multiSelectList() + * TODO: check if multiSelectList() could be possibly be renamed to multiSelectArray() + * + * @param left + * @param right + * @param comparator + * @returns + */ +const multiSelectList = (nodes: unknown[]): ASTThing => ({ + type: 'multi_select_list', + children: nodes, +}); + +/** + * + * @param left + * @param right + * @param comparator + * @returns + */ +const orExpression = (left: unknown, right: unknown): ASTThing => ({ + type: 'or_expression', + children: [left, right], +}); + +/** + * + * @param left + * @param right + * @param comparator + * @returns + */ +const andExpression = (left: unknown, right: unknown): ASTThing => ({ + type: 'and_expression', + children: [left, right], +}); + +/** + * + * @param left + * @param right + * @param comparator + * @returns + */ +const notExpression = (expr: unknown): ASTThing => ({ + type: 'not_expression', + children: [expr], +}); + +/** + * + * @param left + * @param right + * @param comparator + * @returns + */ +const pipe = (left: unknown, right: unknown): ASTThing => ({ + type: 'pipe', + children: [left, right], +}); + +/** + * + * @param left + * @param right + * @param comparator + * @returns + */ +const projection = (left: unknown, right: unknown): ASTThing => ({ + type: 'projection', + children: [left, right], +}); + +/** + * + * @param left + * @param right + * @param comparator + * @returns + */ +const subexpression = (children: unknown[]): ASTThing => ({ + type: 'subexpression', + children: children, +}); + +/** + * + * @param left + * @param right + * @param comparator + * @returns + */ +const slice = (start: unknown, end: unknown, step: unknown): ASTThing => ({ + type: 'slice', + children: [start, end, step], +}); + +/** + * + * @param left + * @param right + * @param comparator + * @returns + */ +const valueProjection = (left: unknown, right: unknown): ASTThing => ({ + type: 'value_projection', + children: [left, right], +}); + +export { + comparator, + currentNode, + expref, + functionExpression, + field, + filterProjection, + flatten, + identity, + index, + indexExpression, + keyValPair, + literal, + multiSelectDict, + multiSelectList, + orExpression, + andExpression, + notExpression, + pipe, + projection, + subexpression, + slice, + valueProjection, +}; diff --git a/packages/jmespath/src/compile.ts b/packages/jmespath/src/compile.ts new file mode 100644 index 0000000000..afa8d122d6 --- /dev/null +++ b/packages/jmespath/src/compile.ts @@ -0,0 +1,23 @@ +import { Parser } from './Parser'; +import type { ParsedResult } from './ParsedResult'; + +/** + * TODO: see if this Expression type should be the return of compile() + */ +/* type Expression = { + type: string; + children: Expression[]; + value: string; +}; */ + +/** + * TODO: write docs for compile() + * TODO: fix types for compile() + * + * @param expression The JMESPath expression to compile. + */ +const compile = (expression: string): ParsedResult => { + return new Parser().parse(expression); +}; + +export { compile }; diff --git a/packages/jmespath/src/constants.ts b/packages/jmespath/src/constants.ts new file mode 100644 index 0000000000..5b6ef3dbfe --- /dev/null +++ b/packages/jmespath/src/constants.ts @@ -0,0 +1,68 @@ +const BINDING_POWER = { + eof: 0, + unquoted_identifier: 0, + quoted_identifier: 0, + literal: 0, + rbracket: 0, + rparen: 0, + comma: 0, + rbrace: 0, + number: 0, + current: 0, + expref: 0, + colon: 0, + pipe: 1, + or: 2, + and: 3, + eq: 5, + gt: 5, + lt: 5, + gte: 5, + lte: 5, + ne: 5, + flatten: 9, + // Everything above stops a projection. + star: 20, + filter: 21, + dot: 40, + not: 45, + lbrace: 50, + lbracket: 55, + lparen: 60, +} as const; + +const ASCII_LOWERCASE = 'abcdefghijklmnopqrstuvwxyz'; +const ASCII_UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +const ASCII_LETTERS = ASCII_LOWERCASE + ASCII_UPPERCASE; +const DIGITS = '0123456789'; + +const START_IDENTIFIER = new Set(ASCII_LETTERS + '_'); +const VALID_IDENTIFIER = new Set(ASCII_LETTERS + DIGITS + '_'); +const VALID_NUMBER = new Set(DIGITS); +const WHITESPACE = new Set(' \t\n\r'); +const SIMPLE_TOKENS = new Map([ + ['.', 'dot'], + ['*', 'star'], + [':', 'colon'], + [']', 'rbracket'], + [',', 'comma'], + [':', 'colon'], + ['@', 'current'], + ['(', 'lparen'], + [')', 'rparen'], + ['{', 'lbrace'], + ['}', 'rbrace'], +]); + +/** + * A map of JavaScript types to JMESPath types. + */ + +export { + BINDING_POWER, + WHITESPACE, + START_IDENTIFIER, + VALID_IDENTIFIER, + VALID_NUMBER, + SIMPLE_TOKENS, +}; diff --git a/packages/jmespath/src/errors.ts b/packages/jmespath/src/errors.ts new file mode 100644 index 0000000000..9944618471 --- /dev/null +++ b/packages/jmespath/src/errors.ts @@ -0,0 +1,181 @@ +/** + * TODO: write docs for JMESPathError + */ +class JMESPathError extends Error { + public constructor(message: string) { + super(message); + this.name = 'JMESPathError'; + } +} + +/** + * TODO: write docs for LexerError + */ +class LexerError extends JMESPathError { + /** + * Expression that was being parsed when the error occurred. + * + * Can be set by whatever catches the error. + */ + public expression?: string; + public lexerPosition: number; + public lexerValue: string; + + public constructor(lexerPosition: number, lexerValue: string) { + super('Bad jmespath expression'); + this.name = 'LexerError'; + this.lexerPosition = lexerPosition; + this.lexerValue = lexerValue; + + // Set the message to include the lexer position and value. + this.message = `${super.message}: ${this.expression} at position ${ + this.lexerPosition + }: ${this.lexerValue}`; + } +} + +/** + * TODO: write docs for ParseError + * + * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/exceptions.py#L9 + */ +class ParseError extends JMESPathError { + /** + * Expression that was being parsed when the error occurred. + * + * Can be set by whatever catches the error. + */ + public expression?: string; + public lexPosition: number; + public reason?: string; + public tokenType: string; + public tokenValue: string; + + public constructor(options: { + lexPosition: number; + tokenValue: string; + tokenType: string; + reason?: string; + }) { + super('Invalid jmespath expression'); + this.name = 'ParseError'; + this.lexPosition = options.lexPosition; + this.tokenValue = options.tokenValue; + this.tokenType = options.tokenType.toUpperCase(); + this.reason = options.reason; + + // Set the message to include the lexer position and token info. + this.message = `${super.message}: ${this.reason}\nParse error at column ${ + this.lexPosition + }, token "${this.tokenValue}" (${this.tokenType}), for expression:\n${ + this.expression + }`; + } +} + +/** + * TODO: complete IncompleteExpressionError implementation + * TODO: write docs for IncompleteExpressionError + * TODO: add `name` to `IncompleteExpressionError` + * + * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/exceptions.py#L32 + */ +class IncompleteExpressionError extends ParseError { + /** + * Expression that was being parsed when the error occurred. + * + * Can be set by whatever catches the error. + */ + public expression?: string; +} + +/** + * TODO: write docs for ArityError + * TODO: complete ArityError implementation + * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/exceptions.py#LL66C1-L85C30 + */ +/* class ArityError extends ParseError { + public actualArity: number; + public expectedArity: number; + public functionName: string; + + public constructor(options: { + expectedArity: number; + actualArity: number; + functionName: string; + }) { + this.name = 'ArityError'; + this.actualArity = options.actualArity; + this.expectedArity = options.expectedArity; + this.functionName = options.functionName; + } + + #pluralize(word: string, count: number): string { + return count === 1 ? word : `${word}s`; + } +} */ + +/** + * TODO: write docs for VariadicArityError + * TODO: complete VariadicArityError implementation + * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/exceptions.py#L89-L96 + * TODO: change extends to ArityError + * TODO: add `name` to `VariadicArityError` + */ +class VariadicArityError extends ParseError {} + +/** + * TODO: write docs for JMESPathTypeError + */ +class JMESPathTypeError extends JMESPathError { + public actualType: string; + public currentValue: unknown; + public expectedTypes: string; + public functionName: string; + + public constructor(options: { + functionName: string; + currentValue: unknown; + actualType: string; + expectedTypes: string; + }) { + super('Invalid type for JMESPath expression'); + this.name = 'JMESPathTypeError'; + this.functionName = options.functionName; + this.currentValue = options.currentValue; + this.actualType = options.actualType; + this.expectedTypes = options.expectedTypes; + + // Set the message to include the error info. + this.message = `${super.message}: function ${ + this.functionName + } expected one of: ${this.expectedTypes}, received: ${this.actualType}`; + } +} + +/** + * TODO: write docs for EmptyExpressionError + */ +class EmptyExpressionError extends JMESPathError { + public constructor() { + super('Invalid JMESPath expression: cannot be empty.'); + this.name = 'EmptyExpressionError'; + } +} + +/** + * TODO: write docs for UnknownFunctionError + */ +class UnknownFunctionError extends JMESPathError {} + +export { + JMESPathError, + LexerError, + ParseError, + IncompleteExpressionError, + // ArityError, + VariadicArityError, + JMESPathTypeError, + EmptyExpressionError, + UnknownFunctionError, +}; diff --git a/packages/jmespath/src/index.ts b/packages/jmespath/src/index.ts new file mode 100644 index 0000000000..5a2bdeb5b8 --- /dev/null +++ b/packages/jmespath/src/index.ts @@ -0,0 +1 @@ +export * from './search'; diff --git a/packages/jmespath/src/search.ts b/packages/jmespath/src/search.ts new file mode 100644 index 0000000000..cdaee25dec --- /dev/null +++ b/packages/jmespath/src/search.ts @@ -0,0 +1,20 @@ +import { Parser } from './Parser'; + +/** + * TODO: write docs for search() + * TODO: fix types for search() + * + * @param expression + * @param data + * @param options + * @returns + */ +const search = ( + expression: string, + data: unknown, + options?: unknown +): unknown => { + return new Parser().parse(expression).search(data, options); +}; + +export { search }; diff --git a/packages/jmespath/src/visitor/GraphvizVisitor.ts b/packages/jmespath/src/visitor/GraphvizVisitor.ts new file mode 100644 index 0000000000..d9363f43ec --- /dev/null +++ b/packages/jmespath/src/visitor/GraphvizVisitor.ts @@ -0,0 +1,14 @@ +class GraphvizVisitor { + /** + * TODO: write docs for GraphvizVisitor.visit() + * TODO: finalize types for GraphvizVisitor.visit() + * + * @param _node + * @returns + */ + public visit(_node: unknown): string { + return ''; + } +} + +export { GraphvizVisitor }; diff --git a/packages/jmespath/src/visitor/TreeInterpreter.ts b/packages/jmespath/src/visitor/TreeInterpreter.ts new file mode 100644 index 0000000000..221a804137 --- /dev/null +++ b/packages/jmespath/src/visitor/TreeInterpreter.ts @@ -0,0 +1,22 @@ +class TreeInterpreter { + /** + * TODO: implement TreeInterpreter constructor + * @param _options + */ + public constructor(_options?: unknown) { + // Do nothing. + } + /** + * TODO: write docs for TreeInterpreter.visit() + * TODO: finalize types for TreeInterpreter.visit() + * + * @param _node + * @param _value + * @returns + */ + public visit(_node: unknown, _value: unknown): unknown { + return ''; + } +} + +export { TreeInterpreter }; diff --git a/packages/jmespath/src/visitor/index.ts b/packages/jmespath/src/visitor/index.ts new file mode 100644 index 0000000000..8a70f8f104 --- /dev/null +++ b/packages/jmespath/src/visitor/index.ts @@ -0,0 +1,2 @@ +export * from './TreeInterpreter'; +export * from './GraphvizVisitor'; diff --git a/packages/jmespath/tests/helpers/populateEnvironmentVariables.ts b/packages/jmespath/tests/helpers/populateEnvironmentVariables.ts new file mode 100644 index 0000000000..18f29d6992 --- /dev/null +++ b/packages/jmespath/tests/helpers/populateEnvironmentVariables.ts @@ -0,0 +1 @@ +// Powertools for AWS Lambda (TypeScript) variables \ No newline at end of file diff --git a/packages/jmespath/tests/unit/base.test.ts b/packages/jmespath/tests/unit/base.test.ts new file mode 100644 index 0000000000..dfcea29b66 --- /dev/null +++ b/packages/jmespath/tests/unit/base.test.ts @@ -0,0 +1,75 @@ +import { search } from '../../src'; + +describe('Base tests', () => { + it.each([ + { expression: 'foo', expected: { bar: { baz: 'qux' } } }, + { expression: 'foo.bar', expected: { baz: 'qux' } }, + { expression: 'foo.bar.baz', expected: 'qux' }, + { expression: 'foo.bar.baz.qux', expected: undefined }, + { expression: 'qux', expected: undefined }, + { expression: 'qux.quux', expected: undefined }, + { expression: 'qux.quux.quuux', expected: undefined }, + { expression: 'ffoo\n.\nbar\n.baz', expected: 'qux' }, + ])('should parse a multi-level nested object', ({ expression, expected }) => { + // Prepare + const data = { foo: { bar: { baz: 'qux' } } }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { expression: 'foo', expected: { bar: ['a', 'b', 'c'] } }, + { expression: 'foo.bar', expected: ['a', 'b', 'c'] }, + { expression: 'foo.bar.a', expected: undefined }, + ])( + 'should parse multi-level objects with arrays', + ({ expression, expected }) => { + // Prepare + const data = { foo: { bar: ['a', 'b', 'c'] } }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { expression: 'a', expected: undefined }, + { expression: 'b', expected: undefined }, + { expression: 'c', expected: undefined }, + { expression: 'a.b', expected: undefined }, + ])('should parse an array', ({ expression, expected }) => { + // Prepare + const data = ['a', 'b', 'c']; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { expression: 'foo."1"', expected: ['a', 'b', 'c'] }, + { expression: 'foo."1"[0]', expected: 'a' }, + { expression: 'foo."-1"', expected: 'bar' }, + ])( + 'should parse an object with arrays and numeric values as keys', + ({ expression, expected }) => { + // Prepare + const data = { foo: { '1': ['a', 'b', 'c'], '-1': 'bar' } }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/current.tests.ts b/packages/jmespath/tests/unit/current.tests.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/jmespath/tests/unit/unicode.test.ts b/packages/jmespath/tests/unit/unicode.test.ts new file mode 100644 index 0000000000..fbec261083 --- /dev/null +++ b/packages/jmespath/tests/unit/unicode.test.ts @@ -0,0 +1,80 @@ +import { search } from '../../src'; + +describe('Base tests', () => { + it.each([ + { + expression: 'foo[]."✓"', + expected: ['✓', '✗'], + }, + ])( + 'should parse an object with unicode chars as keys and values', + ({ expression, expected }) => { + // Prepare + const data = { foo: [{ '✓': '✓' }, { '✓': '✗' }] }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '☯', + expected: true, + }, + { + expression: '☃', + expected: undefined, + }, + ])( + 'should parse an object with unicode chars as keys', + ({ expression, expected }) => { + // Prepare + const data = { '☯': true }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { expression: 'a', expected: undefined }, + { expression: 'b', expected: undefined }, + { expression: 'c', expected: undefined }, + { expression: 'a.b', expected: undefined }, + ])('should parse an array', ({ expression, expected }) => { + // Prepare + const data = ['a', 'b', 'c']; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: '♪♫•*¨*•.¸¸❤¸¸.•*¨*•♫♪', + expected: true, + }, + ])( + 'should parse an object with mulitple unicode chars as keys', + ({ expression, expected }) => { + // Prepare + const data = { '♪♫•*¨*•.¸¸❤¸¸.•*¨*•♫♪': true }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/wildcard.test.ts b/packages/jmespath/tests/unit/wildcard.test.ts new file mode 100644 index 0000000000..98de5b20ef --- /dev/null +++ b/packages/jmespath/tests/unit/wildcard.test.ts @@ -0,0 +1,662 @@ +import { search } from '../../src'; + +describe('Wildcard tests', () => { + it.each([ + { + expression: 'foo.*.baz', + expected: ['val', 'val', 'val'], + }, + { + expression: 'foo.bar.*', + expected: ['val'], + }, + { + expression: 'foo.*.notbaz', + expected: [ + ['a', 'b', 'c'], + ['a', 'b', 'c'], + ], + }, + { + expression: 'foo.*.notbaz[0]', + expected: ['a', 'a'], + }, + { + expression: 'foo.*.notbaz[-1]', + expected: ['c', 'c'], + }, + ])( + 'should parse the wildcard operator with an object containing multiple keys at different levels', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: { + baz: 'val', + }, + other: { + baz: 'val', + }, + other2: { + baz: 'val', + }, + other3: { + notbaz: ['a', 'b', 'c'], + }, + other4: { + notbaz: ['a', 'b', 'c'], + }, + other5: { + other: { + a: 1, + b: 1, + c: 1, + }, + }, + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.*', + expected: [ + { 'second-1': 'val' }, + { 'second-1': 'val' }, + { 'second-1': 'val' }, + ], + }, + { + expression: 'foo.*.*', + expected: [['val'], ['val'], ['val']], + }, + { + expression: 'foo.*.*.*', + expected: [[], [], []], + }, + { + expression: 'foo.*.*.*.*', + expected: [[], [], []], + }, + ])( + 'should parse the wildcard operator with an object containing keys with hyphens', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + 'first-1': { + 'second-1': 'val', + }, + 'first-2': { + 'second-1': 'val', + }, + 'first-3': { + 'second-1': 'val', + }, + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '*.bar', + expected: ['one', 'one'], + }, + ])( + 'should parse the wildcard operator with an object containing multiple keys', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: 'one', + }, + other: { + bar: 'one', + }, + nomatch: { + notbar: 'three', + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '*', + expected: [{ sub1: { foo: 'one' } }, { sub1: { foo: 'one' } }], + }, + { + expression: '*.sub1', + expected: [{ foo: 'one' }, { foo: 'one' }], + }, + { + expression: '*.*', + expected: [[{ foo: 'one' }], [{ foo: 'one' }]], + }, + { + expression: '*.*.foo[]', + expected: ['one', 'one'], + }, + { + expression: '*.sub1.foo', + expected: ['one', 'one'], + }, + ])( + 'should parse the wildcard operator with an object containing nested objects', + ({ expression, expected }) => { + // Prepare + const data = { + top1: { + sub1: { foo: 'one' }, + }, + top2: { + sub1: { foo: 'one' }, + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*].bar', + expected: ['one', 'two', 'three'], + }, + { + expression: 'foo[*].notbar', + expected: ['four'], + }, + ])( + 'should parse the wildcard operator with an object containing an array of objects', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { bar: 'one' }, + { bar: 'two' }, + { bar: 'three' }, + { notbar: 'four' }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '[*]', + expected: [ + { bar: 'one' }, + { bar: 'two' }, + { bar: 'three' }, + { notbar: 'four' }, + ], + }, + { + expression: '[*].bar', + expected: ['one', 'two', 'three'], + }, + { + expression: '[*].notbar', + expected: ['four'], + }, + ])( + 'should parse the wildcard operator with an array of objects', + ({ expression, expected }) => { + // Prepare + const data = [ + { bar: 'one' }, + { bar: 'two' }, + { bar: 'three' }, + { notbar: 'four' }, + ]; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.bar[*].baz', + expected: [ + ['one', 'two', 'three'], + ['four', 'five', 'six'], + ['seven', 'eight', 'nine'], + ], + }, + { + expression: 'foo.bar[*].baz[0]', + expected: ['one', 'four', 'seven'], + }, + { + expression: 'foo.bar[*].baz[1]', + expected: ['two', 'five', 'eight'], + }, + { + expression: 'foo.bar[*].baz[2]', + expected: ['three', 'six', 'nine'], + }, + { + expression: 'foo.bar[*].baz[3]', + expected: [], + }, + ])( + 'should parse the wildcard operator with an object with nested objects containing arrays', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: [ + { baz: ['one', 'two', 'three'] }, + { baz: ['four', 'five', 'six'] }, + { baz: ['seven', 'eight', 'nine'] }, + ], + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.bar[*]', + expected: [ + ['one', 'two'], + ['three', 'four'], + ], + }, + { + expression: 'foo.bar[0]', + expected: ['one', 'two'], + }, + { + expression: 'foo.bar[0][0]', + expected: 'one', + }, + { + expression: 'foo.bar[0][0][0]', + expected: null, + }, + { + expression: 'foo.bar[0][0][0][0]', + expected: null, + }, + { + expression: 'foo[0][0]', + expected: null, + }, + ])( + 'should parse the wildcard operator with an object with nested arrays', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: [ + ['one', 'two'], + ['three', 'four'], + ], + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*].bar[*].kind', + expected: [ + ['basic', 'intermediate'], + ['advanced', 'expert'], + ], + }, + { + expression: 'foo[*].bar[0].kind', + expected: ['basic', 'advanced'], + }, + ])( + 'should parse the wildcard operator with an array of objects with nested arrays or strings', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { bar: [{ kind: 'basic' }, { kind: 'intermediate' }] }, + { bar: [{ kind: 'advanced' }, { kind: 'expert' }] }, + { bar: 'string' }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*].bar.kind', + expected: ['basic', 'intermediate', 'advanced', 'expert'], + }, + ])( + 'should parse the wildcard operator with an array of objects', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { bar: { kind: 'basic' } }, + { bar: { kind: 'intermediate' } }, + { bar: { kind: 'advanced' } }, + { bar: { kind: 'expert' } }, + { bar: 'string' }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*].bar[0]', + expected: ['one', 'three', 'five'], + }, + { + expression: 'foo[*].bar[1]', + expected: ['two', 'four'], + }, + { + expression: 'foo[*].bar[2]', + expected: [], + }, + ])( + 'should parse the wildcard operator with an array of objects with arrays', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { bar: ['one', 'two'] }, + { bar: ['three', 'four'] }, + { bar: ['five'] }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*].bar[0]', + expected: [], + }, + ])( + 'should parse the wildcard operator with an array of objects with empty arrays', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [{ bar: [] }, { bar: [] }, { bar: [] }], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*][0]', + expected: ['one', 'three', 'five'], + }, + { + expression: 'foo[*][1]', + expected: ['two', 'four'], + }, + ])( + 'should parse the wildcard operator with an array of arrays', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [['one', 'two'], ['three', 'four'], ['five']], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*][0]', + expected: [['one', 'two'], ['five', 'six'], ['nine']], + }, + { + expression: 'foo[*][1]', + expected: [['three', 'four'], ['seven', 'eight'], ['ten']], + }, + { + expression: 'foo[*][0][0]', + expected: ['one', 'five', 'nine'], + }, + { + expression: 'foo[*][1][0]', + expected: ['three', 'seven', 'ten'], + }, + { + expression: 'foo[*][0][1]', + expected: ['two', 'six'], + }, + { + expression: 'foo[*][1][1]', + expected: ['four', 'eight'], + }, + { + expression: 'foo[*][2]', + expected: [], + }, + { + expression: 'foo[*][2][2]', + expected: [], + }, + { + expression: 'bar[*]', + expected: null, + }, + { + expression: 'bar[*].baz[*]', + expected: null, + }, + ])('should parse a nested array of arrays', ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + [ + ['one', 'two'], + ['three', 'four'], + ], + [ + ['five', 'six'], + ['seven', 'eight'], + ], + [['nine'], ['ten']], + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'string[*]', + expected: null, + }, + { + expression: 'hash[*]', + expected: null, + }, + { + expression: 'number[*]', + expected: null, + }, + { + expression: 'nullvalue[*]', + expected: null, + }, + { + expression: 'string[*].foo', + expected: null, + }, + { + expression: 'hash[*].foo', + expected: null, + }, + { + expression: 'number[*].foo', + expected: null, + }, + { + expression: 'nullvalue[*].foo', + expected: null, + }, + { + expression: 'nullvalue[*].foo[*].bar', + expected: null, + }, + ])( + 'should parse an object with different value types', + ({ expression, expected }) => { + // Prepare + const data = { + string: 'string', + hash: { foo: 'bar', bar: 'baz' }, + number: 23, + nullvalue: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'string.*', + expected: null, + }, + { + expression: 'hash.*', + expected: ['val', 'val'], + }, + { + expression: 'number.*', + expected: null, + }, + { + expression: 'array.*', + expected: null, + }, + { + expression: 'nullvalue.*', + expected: null, + }, + ])( + 'should parse an object with different value types', + ({ expression, expected }) => { + // Prepare + const data = { + string: 'string', + hash: { foo: 'val', bar: 'val' }, + number: 23, + array: [1, 2, 3], + nullvalue: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + it.each([{ expression: '*[0]', expected: [0, 0] }])( + 'should get the first element of each array', + ({ expression, expected }) => { + // Prepare + const data = { + a: [0, 1, 2], + b: [0, 1, 2], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tsconfig.json b/packages/jmespath/tsconfig.json new file mode 100644 index 0000000000..67b7b1e01e --- /dev/null +++ b/packages/jmespath/tsconfig.json @@ -0,0 +1,39 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "noImplicitAny": true, + "target": "ES2020", + "module": "commonjs", + "declaration": true, + "declarationMap": true, + "outDir": "lib", + "removeComments": false, + "strict": true, + "inlineSourceMap": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "pretty": true, + "baseUrl": "src/", + "rootDirs": [ + "src/" + ], + "esModuleInterop": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "./node_modules" + ], + "watchOptions": { + "watchFile": "useFsEvents", + "watchDirectory": "useFsEvents", + "fallbackPolling": "dynamicPriority" + }, + "lib": [ + "es2020" + ], + "types": [ + "node" + ] +} \ No newline at end of file From 3679907906af803e9efa7a621e0e48ae8291b41a Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sun, 4 Jun 2023 14:41:06 +0000 Subject: [PATCH 043/103] tests: spec compliance tests --- packages/jmespath/tests/unit/boolean.test.ts | 288 ++ packages/jmespath/tests/unit/current.test.ts | 33 + packages/jmespath/tests/unit/current.tests.ts | 0 packages/jmespath/tests/unit/escape.test.ts | 56 + packages/jmespath/tests/unit/filters.test.ts | 16 + .../jmespath/tests/unit/functions.test.ts | 2360 +++++++++++++++++ .../jmespath/tests/unit/identifiers.test.ts | 890 +++++++ packages/jmespath/tests/unit/indices.test.ts | 518 ++++ packages/jmespath/tests/unit/literal.test.ts | 244 ++ .../jmespath/tests/unit/multiselect.test.ts | 575 ++++ packages/jmespath/tests/unit/pipe.test.ts | 182 ++ packages/jmespath/tests/unit/slice.test.ts | 233 ++ packages/jmespath/tests/unit/syntax.test.ts | 804 ++++++ packages/jmespath/tests/unit/unicode.test.ts | 2 +- 14 files changed, 6200 insertions(+), 1 deletion(-) create mode 100644 packages/jmespath/tests/unit/boolean.test.ts create mode 100644 packages/jmespath/tests/unit/current.test.ts delete mode 100644 packages/jmespath/tests/unit/current.tests.ts create mode 100644 packages/jmespath/tests/unit/escape.test.ts create mode 100644 packages/jmespath/tests/unit/filters.test.ts create mode 100644 packages/jmespath/tests/unit/functions.test.ts create mode 100644 packages/jmespath/tests/unit/identifiers.test.ts create mode 100644 packages/jmespath/tests/unit/indices.test.ts create mode 100644 packages/jmespath/tests/unit/literal.test.ts create mode 100644 packages/jmespath/tests/unit/multiselect.test.ts create mode 100644 packages/jmespath/tests/unit/pipe.test.ts create mode 100644 packages/jmespath/tests/unit/slice.test.ts create mode 100644 packages/jmespath/tests/unit/syntax.test.ts diff --git a/packages/jmespath/tests/unit/boolean.test.ts b/packages/jmespath/tests/unit/boolean.test.ts new file mode 100644 index 0000000000..a947a381aa --- /dev/null +++ b/packages/jmespath/tests/unit/boolean.test.ts @@ -0,0 +1,288 @@ +import { search } from '../../src'; + +describe('Boolean tests', () => { + it.each([ + { + expression: 'outer.foo || outer.bar', + expected: 'foo', + }, + { + expression: 'outer.foo||outer.bar', + expected: 'foo', + }, + { + expression: 'outer.bar || outer.baz', + expected: 'bar', + }, + { + expression: 'outer.bar||outer.baz', + expected: 'bar', + }, + { + expression: 'outer.bad || outer.foo', + expected: 'foo', + }, + { + expression: 'outer.bad||outer.foo', + expected: 'foo', + }, + { + expression: 'outer.foo || outer.bad', + expected: 'foo', + }, + { + expression: 'outer.foo||outer.bad', + expected: 'foo', + }, + { + expression: 'outer.bad || outer.alsobad', + expected: null, + }, + { + expression: 'outer.bad||outer.alsobad', + expected: null, + }, + ])('should support boolean OR comparison', ({ expression, expected }) => { + // Prepare + const data = { + outer: { + foo: 'foo', + bar: 'bar', + baz: 'baz', + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'outer.empty_string || outer.foo', + expected: 'foo', + }, + ])( + 'should support multiple boolean OR comparisons', + ({ expression, expected }) => { + // Prepare + const data = { + outer: { + foo: 'foo', + bool: false, + empty_list: [], + empty_string: '', + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'True && False', + expected: false, + }, + { + expression: 'False && True', + expected: false, + }, + { + expression: 'True && True', + expected: true, + }, + { + expression: 'False && False', + expected: false, + }, + { + expression: 'True && Number', + expected: 5, + }, + { + expression: 'Number && True', + expected: true, + }, + { + expression: 'Number && False', + expected: false, + }, + { + expression: 'Number && EmptyList', + expected: [], + }, + { + expression: 'Number && True', + expected: true, + }, + { + expression: 'EmptyList && True', + expected: [], + }, + { + expression: 'EmptyList && False', + expected: [], + }, + { + expression: 'True || False', + expected: true, + }, + { + expression: 'True || True', + expected: true, + }, + { + expression: 'False || True', + expected: true, + }, + { + expression: 'False || False', + expected: false, + }, + { + expression: 'Number || EmptyList', + expected: 5, + }, + { + expression: 'Number || True', + expected: 5, + }, + { + expression: 'Number || True && False', + expected: 5, + }, + { + expression: '(Number || True) && False', + expected: false, + }, + { + expression: 'Number || (True && False)', + expected: 5, + }, + { + expression: '!True', + expected: false, + }, + { + expression: '!False', + expected: true, + }, + { + expression: '!Number', + expected: false, + }, + { + expression: '!EmptyList', + expected: true, + }, + { + expression: 'True && !False', + expected: true, + }, + { + expression: 'True && !EmptyList', + expected: true, + }, + { + expression: '!False && !EmptyList', + expected: true, + }, + { + expression: '!(True && False)', + expected: true, + }, + { + expression: '!Zero', + expected: false, + }, + { + expression: '!!Zero', + expected: true, + }, + ])('should support boolean AND comparison', ({ expression, expected }) => { + // Prepare + const data = { + True: true, + False: false, + Number: 5, + EmptyList: [], + Zero: 0, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'one < two', + expected: true, + }, + { + expression: 'one <= two', + expected: true, + }, + { + expression: 'one == one', + expected: true, + }, + { + expression: 'one == two', + expected: false, + }, + { + expression: 'one > two', + expected: false, + }, + { + expression: 'one >= two', + expected: false, + }, + { + expression: 'one != two', + expected: true, + }, + { + expression: 'one < two && three > one', + expected: true, + }, + { + expression: 'one < two || three > one', + expected: true, + }, + { + expression: 'one < two || three < one', + expected: true, + }, + { + expression: 'two < one || three < one', + expected: false, + }, + ])( + 'should support lesser and equal comparison', + ({ expression, expected }) => { + // Prepare + const data = { + one: 1, + two: 2, + three: 3, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/current.test.ts b/packages/jmespath/tests/unit/current.test.ts new file mode 100644 index 0000000000..4820650f23 --- /dev/null +++ b/packages/jmespath/tests/unit/current.test.ts @@ -0,0 +1,33 @@ +import { search } from '../../src'; + +describe('Current operator tests', () => { + it.each([ + { + expression: '@', + expected: { + foo: [{ name: 'a' }, { name: 'b' }], + bar: { baz: 'qux' }, + }, + }, + { + expression: '@.bar', + expected: { baz: 'qux' }, + }, + { + expression: '@.foo[0]', + expected: { name: 'a' }, + }, + ])('should support the current operator', ({ expression, expected }) => { + // Prepare + const data = { + foo: [{ name: 'a' }, { name: 'b' }], + bar: { baz: 'qux' }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); +}); diff --git a/packages/jmespath/tests/unit/current.tests.ts b/packages/jmespath/tests/unit/current.tests.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/jmespath/tests/unit/escape.test.ts b/packages/jmespath/tests/unit/escape.test.ts new file mode 100644 index 0000000000..9b3f45c3d5 --- /dev/null +++ b/packages/jmespath/tests/unit/escape.test.ts @@ -0,0 +1,56 @@ +import { search } from '../../src'; + +describe('Escape characters tests', () => { + it.each([ + { + expression: '"foo.bar"', + expected: 'dot', + }, + { + expression: '"foo bar"', + expected: 'space', + }, + { + expression: '"foo\\nbar"', + expected: 'newline', + }, + { + expression: '"foo\\"bar"', + expected: 'doublequote', + }, + { + expression: '"c:\\\\\\\\windows\\\\path"', + expected: 'windows', + }, + { + expression: '"/unix/path"', + expected: 'unix', + }, + { + expression: '"\\"\\"\\""', + expected: 'threequotes', + }, + { + expression: '"bar"."baz"', + expected: 'qux', + }, + ])('should support escaping characters', ({ expression, expected }) => { + // Prepare + const data = { + 'foo.bar': 'dot', + 'foo bar': 'space', + 'foo\nbar': 'newline', + 'foo"bar': 'doublequote', + 'c:\\\\windows\\path': 'windows', + '/unix/path': 'unix', + '"""': 'threequotes', + bar: { baz: 'qux' }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); +}); diff --git a/packages/jmespath/tests/unit/filters.test.ts b/packages/jmespath/tests/unit/filters.test.ts new file mode 100644 index 0000000000..1a9002f42e --- /dev/null +++ b/packages/jmespath/tests/unit/filters.test.ts @@ -0,0 +1,16 @@ +import { search } from '../../src'; + +describe('Filer operator tests', () => { + it.each([ + + ])('should support the current operator', ({ expression, expected }) => { + // Prepare + const data = ; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); +}); diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/functions.test.ts new file mode 100644 index 0000000000..db9735dc3e --- /dev/null +++ b/packages/jmespath/tests/unit/functions.test.ts @@ -0,0 +1,2360 @@ +import { search } from '../../src'; + +describe('Functions tests', () => { + it.each([ + { + expression: 'abs(foo)', + expected: 1, + }, + { + expression: 'abs(foo)', + expected: 1, + }, + { + expression: 'abs(array[1])', + expected: 3, + }, + { + expression: 'abs(array[1])', + expected: 3, + }, + { + expression: 'abs(`-24`)', + expected: 24, + }, + { + expression: 'abs(`-24`)', + expected: 24, + }, + ])('should support the abs() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'abs(str)', + error: + 'TypeError: abs() expected argument 1 to be type (number) but received type string instead.', + }, + { + expression: 'abs(`false`)', + error: + 'TypeError: abs() expected argument 1 to be type (number) but received type boolean instead.', + }, + { + expression: 'abs(`1`, `2`)', + error: 'ArgumentError: abs() takes 1 argument but received 2', + }, + { + expression: 'abs()', + error: 'ArgumentError: abs() takes 1 argument but received 0', + }, + ])('abs() function errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in abs() fn errors tests + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'unknown_function(`1`, `2`)', + error: 'Unknown function: unknown_function()', + }, + ])('unknown function errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in unknown fn errors tests + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'avg(numbers)', + expected: 2.75, + }, + ])('should support the avg() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'avg(array)', + error: + 'TypeError: avg() expected argument 1 to be type (Array) but received type array instead.', + }, + { + expression: `avg('abc')`, + error: + 'TypeError: avg() expected argument 1 to be type (Array) but received type string instead.', + }, + { + expression: 'avg(foo)', + error: + 'TypeError: avg() expected argument 1 to be type (Array) but received type number instead.', + }, + { + expression: 'avg(@)', + error: + 'TypeError: avg() expected argument 1 to be type (Array) but received type object instead.', + }, + { + expression: 'avg(strings)', + error: + 'TypeError: avg() expected argument 1 to be type (Array) but received type array instead.', + }, + ])('avg() function errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in avg() fn errors tests + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'ceil(`1.2`)', + expected: 2, + }, + { + expression: 'ceil(decimals[0])', + expected: 2, + }, + { + expression: 'ceil(decimals[1])', + expected: 2, + }, + { + expression: 'ceil(decimals[2])', + expected: -1, + }, + ])('should support the ceil() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: `ceil('string')`, + error: + 'TypeError: ceil() expected argument 1 to be type (number) but received type string instead.', + }, + ])('ceil() function errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in ceil() fn errors tests + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'contains("abc", "a")', + expected: true, + }, + { + expression: 'contains("abc", "d")', + expected: false, + }, + { + expression: 'contains(strings, "a")', + expected: true, + }, + { + expression: 'contains(decimals, `1.2`)', + expected: true, + }, + { + expression: 'contains(decimals, `false`)', + expected: false, + }, + ])('should support the contains() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'contains(`false`, "d")', + error: + 'TypeError: contains() expected argument 1 to be type (string | array) but received type boolean instead.', + }, + ])('contains() function errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in contains() fn errors tests + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: `ends_with(str, 'r')`, + expected: true, + }, + { + expression: `ends_with(str, 'tr')`, + expected: true, + }, + { + expression: `ends_with(str, 'Str')`, + expected: true, + }, + { + expression: `ends_with(str, 'SStr')`, + expected: false, + }, + { + expression: `ends_with(str, 'foo')`, + expected: false, + }, + ])('should support the ends_with() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'ends_with(str, `0`)', + error: + 'TypeError: ends_with() expected argument 2 to be type (string) but received type number instead.', + }, + ])('ends_with() function errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in ends_with() fn errors tests + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'floor(`1.2`)', + expected: 1, + }, + { + expression: 'floor(decimals[0])', + expected: 1, + }, + { + expression: 'floor(foo)', + expected: -1, + }, + ])('should support the floor() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: `floor('string')`, + error: + 'TypeError: floor() expected argument 1 to be type (number) but received type string instead.', + }, + { + expression: 'floor(str)', + error: + 'TypeError: floor() expected argument 1 to be type (number) but received type string instead.', + }, + ])('floor() function errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in floor() fn errors tests + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: `length('abc')`, + expected: 3, + }, + { + expression: `length('✓foo')`, + expected: 4, + }, + { + expression: `length('')`, + expected: 0, + }, + { + expression: 'length(@)', + expected: 12, + }, + { + expression: 'length(strings[0])', + expected: 1, + }, + { + expression: 'length(str)', + expected: 3, + }, + { + expression: 'length(array)', + expected: 6, + }, + { + expression: 'length(objects)', + expected: 2, + }, + { + expression: 'length(strings[0])', + expected: 1, + }, + ])('should support the length() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'length(`false`)', + error: + 'TypeError: length() expected argument 1 to be type (string | array | object) but received type boolean instead.', + }, + { + expression: 'length(foo)', + error: + 'TypeError: length() expected argument 1 to be type (string | array | object) but received type number instead.', + }, + ])('length() function errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in length() fn errors tests + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'max(numbers)', + expected: 5, + }, + { + expression: 'max(decimals)', + expected: 1.2, + }, + { + expression: 'max(strings)', + expected: 'c', + }, + { + expression: 'max(decimals)', + expected: 1.2, + }, + { + expression: 'max(empty_list)', + expected: null, + }, + ])('should support the max() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'max(abc)', + error: + 'TypeError: max() expected argument 1 to be type (Array | Array) but received type null instead.', + }, + { + expression: 'max(array)', + error: + 'TypeError: max() expected argument 1 to be type (Array | Array) but received type array instead.', + }, + ])('max() function errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in max() fn errors tests + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'merge(`{}`)', + expected: {}, + }, + { + expression: 'merge(`{}`, `{}`)', + expected: {}, + }, + { + expression: 'merge(`{"a": 1}`, `{"b": 2}`)', + expected: { + a: 1, + b: 2, + }, + }, + { + expression: 'merge(`{"a": 1}`, `{"a": 2}`)', + expected: { + a: 2, + }, + }, + { + expression: 'merge(`{"a": 1, "b": 2}`, `{"a": 2, "c": 3}`, `{"d": 4}`)', + expected: { + a: 2, + b: 2, + c: 3, + d: 4, + }, + }, + ])('should support the merge() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'min(numbers)', + expected: -1, + }, + { + expression: 'min(decimals)', + expected: -1.5, + }, + { + expression: 'min(empty_list)', + expected: null, + }, + { + expression: 'min(decimals)', + expected: -1.5, + }, + { + expression: 'min(strings)', + expected: 'a', + }, + ])('should support the min() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'min(abc)', + error: + 'TypeError: min() expected argument 1 to be type (Array | Array) but received type null instead.', + }, + { + expression: 'min(array)', + error: + 'TypeError: min() expected argument 1 to be type (Array | Array) but received type array instead.', + }, + ])('min() function errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in min() fn errors tests + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: `type('abc')`, + expected: 'string', + }, + { + expression: 'type(`1.0`)', + expected: 'number', + }, + { + expression: 'type(`2`)', + expected: 'number', + }, + { + expression: 'type(`true`)', + expected: 'boolean', + }, + { + expression: 'type(`false`)', + expected: 'boolean', + }, + { + expression: 'type(`null`)', + expected: 'null', + }, + { + expression: 'type(`[0]`)', + expected: 'array', + }, + { + expression: 'type(`{"a": "b"}`)', + expected: 'object', + }, + { + expression: 'type(@)', + expected: 'object', + }, + ])('should support the type() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'sort(keys(objects))', + expected: ['bar', 'foo'], + }, + + { + expression: 'sort(values(objects))', + expected: ['bar', 'baz'], + }, + { + expression: 'keys(empty_hash)', + expected: [], + }, + { + expression: 'sort(numbers)', + expected: [-1, 3, 4, 5], + }, + { + expression: 'sort(strings)', + expected: ['a', 'b', 'c'], + }, + { + expression: 'sort(decimals)', + expected: [-1.5, 1.01, 1.2], + }, + ])( + 'should support the sort(), key(), and values() functions', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'keys(foo)', + error: + 'TypeError: keys() expected argument 1 to be type (object) but received type number instead.', + }, + { + expression: 'keys(strings)', + error: + 'TypeError: keys() expected argument 1 to be type (object) but received type array instead.', + }, + { + expression: 'keys(`false`)', + error: + 'TypeError: keys() expected argument 1 to be type (object) but received type boolean instead.', + }, + { + expression: 'values(foo)', + error: + 'TypeError: values() expected argument 1 to be type (object) but received type number instead.', + }, + { + expression: 'sort(array)', + error: + 'TypeError: sort() expected argument 1 to be type (Array | Array) but received type array instead.', + }, + { + expression: 'sort(abc)', + error: + 'TypeError: sort() expected argument 1 to be type (Array | Array) but received type null instead.', + }, + { + expression: 'sort(empty_list)', + expected: [], + }, + { + expression: 'sort(@)', + error: + 'TypeError: sort() expected argument 1 to be type (Array | Array) but received type object instead.', + }, + ])( + 'sort(), keys(), and values() function errors', + ({ expression, error }) => { + // TODO: see if we can assert the error type as well in sort(), keys(), values() errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + } + ); + + it.each([ + { + expression: `join(', ', strings)`, + expected: 'a, b, c', + }, + { + expression: `join(', ', strings)`, + expected: 'a, b, c', + }, + { + expression: 'join(\',\', `["a", "b"]`)', + expected: 'a,b', + }, + { + expression: `join('|', strings)`, + expected: 'a|b|c', + }, + { + expression: `join('|', decimals[].to_string(@))`, + expected: '1.01|1.2|-1.5', + }, + { + expression: `join('|', empty_list)`, + expected: '', + }, + ])('should support the join() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'join(\',\', `["a", 0]`)', + error: + 'TypeError: join() expected argument 2 to be type (Array) but received type array instead.', + }, + { + expression: `join(', ', str)`, + error: + 'TypeError: join() expected argument 2 to be type (Array) but received type string instead.', + }, + { + expression: 'join(`2`, strings)', + error: + 'TypeError: join() expected argument 1 to be type (string) but received type number instead.', + }, + { + expression: `join('|', decimals)`, + error: + 'TypeError: join() expected argument 2 to be type (Array) but received type array instead.', + }, + ])('join() function errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in join() errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'reverse(numbers)', + expected: [5, 4, 3, -1], + }, + { + expression: 'reverse(array)', + expected: ['100', 'a', 5, 4, 3, -1], + }, + { + expression: 'reverse(`[]`)', + expected: [], + }, + { + expression: `reverse('')`, + expected: '', + }, + { + expression: `reverse('hello world')`, + expected: 'dlrow olleh', + }, + ])('should support the reverse() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: `starts_with(str, 'S')`, + expected: true, + }, + { + expression: `starts_with(str, 'St')`, + expected: true, + }, + { + expression: `starts_with(str, 'Str')`, + expected: true, + }, + { + expression: `starts_with(str, 'String')`, + expected: false, + }, + ])( + 'should support the starts_with() function', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'starts_with(str, `0`)', + error: + 'TypeError: starts_with() expected argument 2 to be type (string) but received type number instead.', + }, + ])('starts_with() function errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in starts_with() errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'sum(numbers)', + expected: 11, + }, + { + expression: 'sum(decimals)', + expected: 0.71, + }, + { + expression: 'sum(array[].to_number(@))', + expected: 111, + }, + { + expression: 'sum(`[]`)', + expected: 0, + }, + ])('should support the sum() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'sum(array)', + error: + 'TypeError: sum() expected argument 1 to be type (Array) but received type array instead.', + }, + ])('sum() function errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in sum() errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: `to_array('foo')`, + expected: ['foo'], + }, + { + expression: 'to_array(`0`)', + expected: [0], + }, + { + expression: 'to_array(objects)', + expected: [ + { + foo: 'bar', + bar: 'baz', + }, + ], + }, + { + expression: 'to_array(`[1, 2, 3]`)', + expected: [1, 2, 3], + }, + { + expression: 'to_array(false)', + expected: [false], + }, + ])('should support the to_array() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: `to_string('foo')`, + expected: 'foo', + }, + { + expression: 'to_string(`1.2`)', + expected: '1.2', + }, + { + expression: 'to_string(`[0, 1]`)', + expected: '[0,1]', + }, + { + description: 'function projection on single arg function', + expression: 'numbers[].to_string(@)', + expected: ['-1', '3', '4', '5'], + }, + ])('should support the to_string() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: `to_number('1.0')`, + expected: 1.0, + }, + { + expression: `to_number('1.1')`, + expected: 1.1, + }, + { + expression: `to_number('4')`, + expected: 4, + }, + { + expression: `to_number('notanumber')`, + expected: null, + }, + { + expression: 'to_number(`false`)', + expected: null, + }, + { + expression: 'to_number(`null`)', + expected: null, + }, + { + expression: 'to_number(`[0]`)', + expected: null, + }, + { + expression: 'to_number(`{"foo": 0}`)', + expected: null, + }, + { + description: 'function projection on single arg function', + expression: 'array[].to_number(@)', + expected: [-1, 3, 4, 5, 100], + }, + ])('should support the to_number() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: '"to_string"(`1.0`)', + error: 'Quoted identifier not allowed for function names.', + }, + ])('to_number() function errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in to_number() errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'not_null(unknown_key, str)', + expected: 'Str', + }, + { + expression: 'not_null(unknown_key, foo.bar, empty_list, str)', + expected: [], + }, + { + expression: 'not_null(unknown_key, null_key, empty_list, str)', + expected: [], + }, + { + expression: 'not_null(all, expressions, are_null)', + expected: null, + }, + ])('should support the not_null() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'not_null()', + error: + 'ArgumentError: not_null() takes at least 1 argument but received 0', + }, + ])('not_null() function errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in not_null() errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + description: 'function projection on variadic function', + expression: 'foo[].not_null(f, e, d, c, b, a)', + expected: ['b', 'c', 'd', 'e', 'f'], + }, + ])( + 'should support function projection on variadic function', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { + b: 'b', + a: 'a', + }, + { + c: 'c', + b: 'b', + }, + { + d: 'd', + c: 'c', + }, + { + e: 'e', + d: 'd', + }, + { + f: 'f', + e: 'e', + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + description: 'sort by field expression', + expression: 'sort_by(people, &age)', + expected: [ + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + ], + }, + { + expression: 'sort_by(people, &age_str)', + expected: [ + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + ], + }, + { + description: 'sort by function expression', + expression: 'sort_by(people, &to_number(age_str))', + expected: [ + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + ], + }, + { + description: 'function projection on sort_by function', + expression: 'sort_by(people, &age)[].name', + expected: [3, 'a', 'c', 'b', 'd'], + }, + + { + expression: 'sort_by(people, &age)[].extra', + expected: ['foo', 'bar'], + }, + { + expression: 'sort_by(`[]`, &age)', + expected: [], + }, + ])('should support sorty_by() special cases', ({ expression, expected }) => { + // Prepare + const data = { + people: [ + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'sort_by(people, &extra)', + error: 'TypeError: expected (string), received null', + }, + { + expression: 'sort_by(people, &bool)', + error: 'TypeError: unexpected type (boolean)', + }, + { + expression: 'sort_by(people, &name)', + error: 'TypeError: expected (string), received number', + }, + { + expression: 'sort_by(people, name)', + error: + 'TypeError: sort_by() expected argument 2 to be type (expression) but received type null instead.', + }, + ])('sort_by() function special cases errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in sort_by() function special cases errors tests + // Prepare + const data = { + people: [ + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + ], + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'max_by(people, &age)', + expected: { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + }, + { + expression: 'max_by(people, &age_str)', + expected: { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + }, + { + expression: 'max_by(people, &to_number(age_str))', + expected: { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + }, + ])('should support max_by() special cases', ({ expression, expected }) => { + // Prepare + const data = { + people: [ + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'max_by(people, &bool)', + error: 'TypeError: expected one of (number | string), received boolean', + }, + { + expression: 'max_by(people, &extra)', + error: 'TypeError: expected one of (number | string), received null', + }, + ])('max_by() function special cases errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in max_by() function special cases errors tests + // Prepare + const data = { + people: [ + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + ], + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'min_by(people, &age)', + expected: { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + }, + { + expression: 'min_by(people, &age_str)', + expected: { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + }, + { + expression: 'min_by(people, &to_number(age_str))', + expected: { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + }, + ])('should support min_by() special cases', ({ expression, expected }) => { + // Prepare + const data = { + people: [ + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'min_by(people, &bool)', + error: 'TypeError: expected one of (number | string), received boolean', + }, + { + expression: 'min_by(people, &extra)', + error: 'TypeError: expected one of (number | string), received null', + }, + ])('min_by() function special cases errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in min_by() function special cases errors tests + // Prepare + const data = { + people: [ + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + ], + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + description: 'stable sort order', + expression: 'sort_by(people, &age)', + expected: [ + { + age: 10, + order: '1', + }, + { + age: 10, + order: '2', + }, + { + age: 10, + order: '3', + }, + { + age: 10, + order: '4', + }, + { + age: 10, + order: '5', + }, + { + age: 10, + order: '6', + }, + { + age: 10, + order: '7', + }, + { + age: 10, + order: '8', + }, + { + age: 10, + order: '9', + }, + { + age: 10, + order: '10', + }, + { + age: 10, + order: '11', + }, + ], + }, + ])('should support stable sort_by() order', ({ expression, expected }) => { + // Prepare + const data = { + people: [ + { + age: 10, + order: '1', + }, + { + age: 10, + order: '2', + }, + { + age: 10, + order: '3', + }, + { + age: 10, + order: '4', + }, + { + age: 10, + order: '5', + }, + { + age: 10, + order: '6', + }, + { + age: 10, + order: '7', + }, + { + age: 10, + order: '8', + }, + { + age: 10, + order: '9', + }, + { + age: 10, + order: '10', + }, + { + age: 10, + order: '11', + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'map(&a, people)', + expected: [10, 10, 10, 10, 10, 10, 10, 10, 10], + }, + { + expression: 'map(&c, people)', + expected: ['z', null, null, 'z', null, null, 'z', null, null], + }, + ])('should support map() special cases', ({ expression, expected }) => { + // Prepare + const data = { + people: [ + { + a: 10, + b: 1, + c: 'z', + }, + { + a: 10, + b: 2, + c: null, + }, + { + a: 10, + b: 3, + }, + { + a: 10, + b: 4, + c: 'z', + }, + { + a: 10, + b: 5, + c: null, + }, + { + a: 10, + b: 6, + }, + { + a: 10, + b: 7, + c: 'z', + }, + { + a: 10, + b: 8, + c: null, + }, + { + a: 10, + b: 9, + }, + ], + empty: [], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'map(&a, badkey)', + error: + 'TypeError: map() expected argument 2 to be type (array) but received type null instead.', + }, + { + expression: 'map(&foo, empty)', + expected: [], + }, + ])('map() function special cases errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in map() function special cases errors tests + // Prepare + const data = { + people: [ + { + a: 10, + b: 1, + c: 'z', + }, + { + a: 10, + b: 2, + c: null, + }, + { + a: 10, + b: 3, + }, + { + a: 10, + b: 4, + c: 'z', + }, + { + a: 10, + b: 5, + c: null, + }, + { + a: 10, + b: 6, + }, + { + a: 10, + b: 7, + c: 'z', + }, + { + a: 10, + b: 8, + c: null, + }, + { + a: 10, + b: 9, + }, + ], + empty: [], + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'map(&foo.bar, array)', + expected: ['yes1', 'yes2', null], + }, + { + expression: 'map(&foo1.bar, array)', + expected: [null, null, 'no'], + }, + { + expression: 'map(&foo.bar.baz, array)', + expected: [null, null, null], + }, + ])( + 'should support map() with the `&` expression cases', + ({ expression, expected }) => { + // Prepare + const data = { + array: [ + { + foo: { + bar: 'yes1', + }, + }, + { + foo: { + bar: 'yes2', + }, + }, + { + foo1: { + bar: 'no', + }, + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'map(&[], array)', + expected: [ + [1, 2, 3, 4], + [5, 6, 7, 8, 9], + ], + }, + ])('should support map() with `&` and `[]`', ({ expression, expected }) => { + // Prepare + const data = { + array: [ + [1, 2, 3, [4]], + [5, 6, 7, [8, 9]], + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); +}); diff --git a/packages/jmespath/tests/unit/identifiers.test.ts b/packages/jmespath/tests/unit/identifiers.test.ts new file mode 100644 index 0000000000..7ac452736a --- /dev/null +++ b/packages/jmespath/tests/unit/identifiers.test.ts @@ -0,0 +1,890 @@ +import { search } from '../../src'; + +describe('Identifiers tests', () => { + it.each([ + { + data: { + __L: true, + }, + expression: '__L', + expected: true, + }, + { + data: { + '!\r': true, + }, + expression: '"!\\r"', + expected: true, + }, + { + data: { + Y_1623: true, + }, + expression: 'Y_1623', + expected: true, + }, + { + data: { + x: true, + }, + expression: 'x', + expected: true, + }, + { + data: { + '\tF\uCebb': true, + }, + expression: '"\\tF\\uCebb"', + expected: true, + }, + { + data: { + ' \t': true, + }, + expression: '" \\t"', + expected: true, + }, + { + data: { + ' ': true, + }, + expression: '" "', + expected: true, + }, + { + data: { + v2: true, + }, + expression: 'v2', + expected: true, + }, + { + data: { + '\t': true, + }, + expression: '"\\t"', + expected: true, + }, + { + data: { + _X: true, + }, + expression: '_X', + expected: true, + }, + { + data: { + '\t4\ud9da\udd15': true, + }, + expression: '"\\t4\\ud9da\\udd15"', + expected: true, + }, + { + data: { + v24_W: true, + }, + expression: 'v24_W', + expected: true, + }, + { + data: { + H: true, + }, + expression: '"H"', + expected: true, + }, + { + data: { + '\f': true, + }, + expression: '"\\f"', + expected: true, + }, + { + data: { + E4: true, + }, + expression: '"E4"', + expected: true, + }, + { + data: { + '!': true, + }, + expression: '"!"', + expected: true, + }, + { + data: { + tM: true, + }, + expression: 'tM', + expected: true, + }, + { + data: { + ' [': true, + }, + expression: '" ["', + expected: true, + }, + { + data: { + 'R!': true, + }, + expression: '"R!"', + expected: true, + }, + { + data: { + _6W: true, + }, + expression: '_6W', + expected: true, + }, + { + data: { + '\uaBA1\r': true, + }, + expression: '"\\uaBA1\\r"', + expected: true, + }, + { + data: { + tL7: true, + }, + expression: 'tL7', + expected: true, + }, + { + data: { + '<': true, + }, + expression: '">"', + expected: true, + }, + { + data: { + hvu: true, + }, + expression: 'hvu', + expected: true, + }, + { + data: { + '; !': true, + }, + expression: '"; !"', + expected: true, + }, + { + data: { + hU: true, + }, + expression: 'hU', + expected: true, + }, + { + data: { + '!I\n/': true, + }, + expression: '"!I\\n\\/"', + expected: true, + }, + { + data: { + '\uEEbF': true, + }, + expression: '"\\uEEbF"', + expected: true, + }, + { + data: { + 'U)\t': true, + }, + expression: '"U)\\t"', + expected: true, + }, + { + data: { + fa0_9: true, + }, + expression: 'fa0_9', + expected: true, + }, + { + data: { + '/': true, + }, + expression: '"/"', + expected: true, + }, + { + data: { + Gy: true, + }, + expression: 'Gy', + expected: true, + }, + { + data: { + '\b': true, + }, + expression: '"\\b"', + expected: true, + }, + { + data: { + '<': true, + }, + expression: '"<"', + expected: true, + }, + { + data: { + '\t': true, + }, + expression: '"\\t"', + expected: true, + }, + { + data: { + '\t&\\\r': true, + }, + expression: '"\\t&\\\\\\r"', + expected: true, + }, + { + data: { + '#': true, + }, + expression: '"#"', + expected: true, + }, + { + data: { + B__: true, + }, + expression: 'B__', + expected: true, + }, + { + data: { + '\nS \n': true, + }, + expression: '"\\nS \\n"', + expected: true, + }, + { + data: { + Bp: true, + }, + expression: 'Bp', + expected: true, + }, + { + data: { + ',\t;': true, + }, + expression: '",\\t;"', + expected: true, + }, + { + data: { + B_q: true, + }, + expression: 'B_q', + expected: true, + }, + { + data: { + '/+\t\n\b!Z': true, + }, + expression: '"\\/+\\t\\n\\b!Z"', + expected: true, + }, + { + data: { + '\udadd\udfc7\\ueFAc': true, + }, + expression: '"\udadd\udfc7\\\\ueFAc"', + expected: true, + }, + { + data: { + ':\f': true, + }, + expression: '":\\f"', + expected: true, + }, + { + data: { + '/': true, + }, + expression: '"\\/"', + expected: true, + }, + { + data: { + _BW_6Hg_Gl: true, + }, + expression: '_BW_6Hg_Gl', + expected: true, + }, + { + data: { + '\udbcf\udc02': true, + }, + expression: '"\udbcf\udc02"', + expected: true, + }, + { + data: { + zs1DC: true, + }, + expression: 'zs1DC', + expected: true, + }, + { + data: { + __434: true, + }, + expression: '__434', + expected: true, + }, + { + data: { + '\udb94\udd41': true, + }, + expression: '"\udb94\udd41"', + expected: true, + }, + { + data: { + Z_5: true, + }, + expression: 'Z_5', + expected: true, + }, + { + data: { + z_M_: true, + }, + expression: 'z_M_', + expected: true, + }, + { + data: { + YU_2: true, + }, + expression: 'YU_2', + expected: true, + }, + { + data: { + _0: true, + }, + expression: '_0', + expected: true, + }, + { + data: { + '\b+': true, + }, + expression: '"\\b+"', + expected: true, + }, + { + data: { + '"': true, + }, + expression: '"\\""', + expected: true, + }, + { + data: { + D7: true, + }, + expression: 'D7', + expected: true, + }, + { + data: { + _62L: true, + }, + expression: '_62L', + expected: true, + }, + { + data: { + '\tK\t': true, + }, + expression: '"\\tK\\t"', + expected: true, + }, + { + data: { + '\n\\\f': true, + }, + expression: '"\\n\\\\\\f"', + expected: true, + }, + { + data: { + I_: true, + }, + expression: 'I_', + expected: true, + }, + { + data: { + W_a0_: true, + }, + expression: 'W_a0_', + expected: true, + }, + { + data: { + BQ: true, + }, + expression: 'BQ', + expected: true, + }, + { + data: { + '\tX$\uABBb': true, + }, + expression: '"\\tX$\\uABBb"', + expected: true, + }, + { + data: { + Z9: true, + }, + expression: 'Z9', + expected: true, + }, + { + data: { + '\b%"\uda38\udd0f': true, + }, + expression: '"\\b%\\"\uda38\udd0f"', + expected: true, + }, + { + data: { + _F: true, + }, + expression: '_F', + expected: true, + }, + { + data: { + '!,': true, + }, + expression: '"!,"', + expected: true, + }, + { + data: { + '"!': true, + }, + expression: '"\\"!"', + expected: true, + }, + { + data: { + Hh: true, + }, + expression: 'Hh', + expected: true, + }, + { + data: { + '&': true, + }, + expression: '"&"', + expected: true, + }, + { + data: { + '9\r\\R': true, + }, + expression: '"9\\r\\\\R"', + expected: true, + }, + { + data: { + M_k: true, + }, + expression: 'M_k', + expected: true, + }, + { + data: { + '!\b\n\udb06\ude52""': true, + }, + expression: '"!\\b\\n\udb06\ude52\\"\\""', + expected: true, + }, + { + data: { + '6': true, + }, + expression: '"6"', + expected: true, + }, + { + data: { + _7: true, + }, + expression: '_7', + expected: true, + }, + { + data: { + '0': true, + }, + expression: '"0"', + expected: true, + }, + { + data: { + '\\8\\': true, + }, + expression: '"\\\\8\\\\"', + expected: true, + }, + { + data: { + b7eo: true, + }, + expression: 'b7eo', + expected: true, + }, + { + data: { + xIUo9: true, + }, + expression: 'xIUo9', + expected: true, + }, + { + data: { + '5': true, + }, + expression: '"5"', + expected: true, + }, + { + data: { + '?': true, + }, + expression: '"?"', + expected: true, + }, + { + data: { + sU: true, + }, + expression: 'sU', + expected: true, + }, + { + data: { + 'VH2&H\\/': true, + }, + expression: '"VH2&H\\\\\\/"', + expected: true, + }, + { + data: { + _C: true, + }, + expression: '_C', + expected: true, + }, + { + data: { + _: true, + }, + expression: '_', + expected: true, + }, + { + data: { + '<\t': true, + }, + expression: '"<\\t"', + expected: true, + }, + { + data: { + '\uD834\uDD1E': true, + }, + expression: '"\\uD834\\uDD1E"', + expected: true, + }, + ])( + 'should handle different identifiers', + ({ data, expression, expected }) => { + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/indices.test.ts b/packages/jmespath/tests/unit/indices.test.ts new file mode 100644 index 0000000000..367fc6feb5 --- /dev/null +++ b/packages/jmespath/tests/unit/indices.test.ts @@ -0,0 +1,518 @@ +import { search } from '../../src'; + +describe('Indices tests', () => { + it.each([ + { + expression: 'foo.bar[0]', + expected: 'zero', + }, + { + expression: 'foo.bar[1]', + expected: 'one', + }, + { + expression: 'foo.bar[2]', + expected: 'two', + }, + { + expression: 'foo.bar[3]', + expected: null, + }, + { + expression: 'foo.bar[-1]', + expected: 'two', + }, + { + expression: 'foo.bar[-2]', + expected: 'one', + }, + { + expression: 'foo.bar[-3]', + expected: 'zero', + }, + { + expression: 'foo.bar[-4]', + expected: null, + }, + ])( + 'should support indices on arrays in a nested object', + ({ expression, expected }) => { + // Prepare + const data = { foo: { bar: ['zero', 'one', 'two'] } }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.bar', + expected: null, + }, + { + expression: 'foo[0].bar', + expected: 'one', + }, + { + expression: 'foo[1].bar', + expected: 'two', + }, + { + expression: 'foo[2].bar', + expected: 'three', + }, + { + expression: 'foo[3].notbar', + expected: 'four', + }, + { + expression: 'foo[3].bar', + expected: null, + }, + { + expression: 'foo[0]', + expected: { bar: 'one' }, + }, + { + expression: 'foo[1]', + expected: { bar: 'two' }, + }, + { + expression: 'foo[2]', + expected: { bar: 'three' }, + }, + { + expression: 'foo[3]', + expected: { notbar: 'four' }, + }, + { + expression: 'foo[4]', + expected: null, + }, + ])( + 'should support indices in an array with objects inside', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { bar: 'one' }, + { bar: 'two' }, + { bar: 'three' }, + { notbar: 'four' }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '[0]', + expected: 'one', + }, + { + expression: '[1]', + expected: 'two', + }, + { + expression: '[2]', + expected: 'three', + }, + { + expression: '[-1]', + expected: 'three', + }, + { + expression: '[-2]', + expected: 'two', + }, + { + expression: '[-3]', + expected: 'one', + }, + ])('should support indices in an array', ({ expression, expected }) => { + // Prepare + const data = ['one', 'two', 'three']; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'reservations[].instances[].foo', + expected: [1, 2], + }, + { + expression: 'reservations[].instances[].bar', + expected: [], + }, + { + expression: 'reservations[].notinstances[].foo', + expected: [], + }, + { + expression: 'reservations[].notinstances[].foo', + expected: [], + }, + ])( + 'should support indices in multi-level nested arrays & objects', + ({ expression, expected }) => { + // Prepare + const data = { reservations: [{ instances: [{ foo: 1 }, { foo: 2 }] }] }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'reservations[].instances[].foo[].bar', + expected: [1, 2, 4, 5, 6, 8], + }, + { + expression: 'reservations[].instances[].foo[].baz', + expected: [], + }, + { + expression: 'reservations[].instances[].notfoo[].bar', + expected: [20, 21, 22, 23, 24, 25], + }, + { + expression: 'reservations[].instances[].notfoo[].notbar', + expected: [[7], [7]], + }, + { + expression: 'reservations[].notinstances[].foo', + expected: [], + }, + { + expression: 'reservations[].instances[].foo[].notbar', + expected: [3, [7]], + }, + { + expression: 'reservations[].instances[].bar[].baz', + expected: [[1], [2], [3], [4]], + }, + { + expression: 'reservations[].instances[].baz[].baz', + expected: [[1, 2], [], [], [3, 4]], + }, + { + expression: 'reservations[].instances[].qux[].baz', + expected: [[], [1, 2, 3], [4], [], [], [1, 2, 3], [4], []], + }, + { + expression: 'reservations[].instances[].qux[].baz[]', + expected: [1, 2, 3, 4, 1, 2, 3, 4], + }, + ])( + 'should support indices in large mixed objects and arrays', + ({ expression, expected }) => { + // Prepare + const data = { + reservations: [ + { + instances: [ + { foo: [{ bar: 1 }, { bar: 2 }, { notbar: 3 }, { bar: 4 }] }, + { foo: [{ bar: 5 }, { bar: 6 }, { notbar: [7] }, { bar: 8 }] }, + { foo: 'bar' }, + { + notfoo: [ + { bar: 20 }, + { bar: 21 }, + { notbar: [7] }, + { bar: 22 }, + ], + }, + { bar: [{ baz: [1] }, { baz: [2] }, { baz: [3] }, { baz: [4] }] }, + { + baz: [ + { baz: [1, 2] }, + { baz: [] }, + { baz: [] }, + { baz: [3, 4] }, + ], + }, + { + qux: [ + { baz: [] }, + { baz: [1, 2, 3] }, + { baz: [4] }, + { baz: [] }, + ], + }, + ], + otherkey: { + foo: [{ bar: 1 }, { bar: 2 }, { notbar: 3 }, { bar: 4 }], + }, + }, + { + instances: [ + { a: [{ bar: 1 }, { bar: 2 }, { notbar: 3 }, { bar: 4 }] }, + { b: [{ bar: 5 }, { bar: 6 }, { notbar: [7] }, { bar: 8 }] }, + { c: 'bar' }, + { + notfoo: [ + { bar: 23 }, + { bar: 24 }, + { notbar: [7] }, + { bar: 25 }, + ], + }, + { + qux: [ + { baz: [] }, + { baz: [1, 2, 3] }, + { baz: [4] }, + { baz: [] }, + ], + }, + ], + otherkey: { + foo: [{ bar: 1 }, { bar: 2 }, { notbar: 3 }, { bar: 4 }], + }, + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[]', + expected: [ + ['one', 'two'], + ['three', 'four'], + ['five', 'six'], + ['seven', 'eight'], + ['nine'], + ['ten'], + ], + }, + { + expression: 'foo[][0]', + expected: ['one', 'three', 'five', 'seven', 'nine', 'ten'], + }, + { + expression: 'foo[][1]', + expected: ['two', 'four', 'six', 'eight'], + }, + { + expression: 'foo[][0][0]', + expected: [], + }, + { + expression: 'foo[][2][2]', + expected: [], + }, + { + expression: 'foo[][0][0][100]', + expected: [], + }, + ])( + 'should support indices in objects containing an array of matrixes', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + [ + ['one', 'two'], + ['three', 'four'], + ], + [ + ['five', 'six'], + ['seven', 'eight'], + ], + [['nine'], ['ten']], + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo', + expected: [ + { + bar: [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + ], + }, + { + bar: [ + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + }, + ], + }, + { + expression: 'foo[]', + expected: [ + { + bar: [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + ], + }, + { + bar: [ + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + }, + ], + }, + { + expression: 'foo[].bar', + expected: [ + [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + ], + [ + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + ], + }, + { + expression: 'foo[].bar[]', + expected: [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + }, + { + expression: 'foo[].bar[].baz', + expected: [1, 3, 5, 7], + }, + ])( + 'should support indices with nested arrays and objects at different levels', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { + bar: [ + { + qux: 2, + baz: 1, + }, + { + qux: 4, + baz: 3, + }, + ], + }, + { + bar: [ + { + qux: 6, + baz: 5, + }, + { + qux: 8, + baz: 7, + }, + ], + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'string[]', + expected: null, + }, + { + expression: 'hash[]', + expected: null, + }, + { + expression: 'number[]', + expected: null, + }, + { + expression: 'nullvalue[]', + expected: null, + }, + { + expression: 'string[].foo', + expected: null, + }, + { + expression: 'hash[].foo', + expected: null, + }, + { + expression: 'number[].foo', + expected: null, + }, + { + expression: 'nullvalue[].foo', + expected: null, + }, + { + expression: 'nullvalue[].foo[].bar', + expected: null, + }, + ])( + 'should support indices in objects having special names as keys', + ({ expression, expected }) => { + // Prepare + const data = { + string: 'string', + hash: { foo: 'bar', bar: 'baz' }, + number: 23, + nullvalue: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/literal.test.ts b/packages/jmespath/tests/unit/literal.test.ts new file mode 100644 index 0000000000..53382bcd90 --- /dev/null +++ b/packages/jmespath/tests/unit/literal.test.ts @@ -0,0 +1,244 @@ +import { search } from '../../src'; + +describe('Literal expressions tests', () => { + it.each([ + { + expression: '`"foo"`', + expected: 'foo', + }, + { + comment: 'Interpret escaped unicode.', + expression: '`"\\u03a6"`', + expected: 'Φ', + }, + { + expression: '`"✓"`', + expected: '✓', + }, + { + expression: '`[1, 2, 3]`', + expected: [1, 2, 3], + }, + { + expression: '`{"a": "b"}`', + expected: { + a: 'b', + }, + }, + { + expression: '`true`', + expected: true, + }, + { + expression: '`false`', + expected: false, + }, + { + expression: '`null`', + expected: null, + }, + { + expression: '`0`', + expected: 0, + }, + { + expression: '`1`', + expected: 1, + }, + { + expression: '`2`', + expected: 2, + }, + { + expression: '`3`', + expected: 3, + }, + { + expression: '`4`', + expected: 4, + }, + { + expression: '`5`', + expected: 5, + }, + { + expression: '`6`', + expected: 6, + }, + { + expression: '`7`', + expected: 7, + }, + { + expression: '`8`', + expected: 8, + }, + { + expression: '`9`', + expected: 9, + }, + { + comment: 'Escaping a backtick in quotes', + expression: '`"foo\\`bar"`', + expected: 'foo`bar', + }, + { + comment: 'Double quote in literal', + expression: '`"foo\\"bar"`', + expected: 'foo"bar', + }, + { + expression: '`"1\\`"`', + expected: '1`', + }, + { + comment: 'Multiple literal expressions with escapes', + expression: '`"\\\\"`.{a:`"b"`}', + expected: { + a: 'b', + }, + }, + { + comment: 'literal . identifier', + expression: '`{"a": "b"}`.a', + expected: 'b', + }, + { + comment: 'literal . identifier . identifier', + expression: '`{"a": {"b": "c"}}`.a.b', + expected: 'c', + }, + { + comment: 'literal . identifier bracket-expr', + expression: '`[0, 1, 2]`[1]', + expected: 1, + }, + ])('should support literal expressions', ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { + name: 'a', + }, + { + name: 'b', + }, + ], + bar: { + baz: 'qux', + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + comment: 'Literal with leading whitespace', + expression: '` {"foo": true}`', + expected: { + foo: true, + }, + }, + { + comment: 'Literal with trailing whitespace', + expression: '`{"foo": true} `', + expected: { + foo: true, + }, + }, + ])( + 'should support literals with other special characters', + ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Literal on RHS of subexpr not allowed', + expression: 'foo.`"bar"`', + error: 'Syntax error, unexpected token: bar(Literal)', + }, + ])('literals errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in literal errors errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: `'foo'`, + expected: 'foo', + }, + { + expression: `' foo '`, + expected: ' foo ', + }, + { + expression: `'0'`, + expected: '0', + }, + { + expression: `'newline\n'`, + expected: 'newline\n', + }, + { + expression: `'\n'`, + expected: '\n', + }, + { + expression: `'✓'`, + expected: '✓', + }, + { + expression: `'𝄞'`, + expected: '𝄞', + }, + { + expression: `' [foo] '`, + expected: ' [foo] ', + }, + { + expression: `'[foo]'`, + expected: '[foo]', + }, + { + comment: 'Do not interpret escaped unicode.', + expression: `'\\u03a6'`, + expected: '\\u03a6', + }, + { + comment: 'Can escape the single quote', + expression: `'foo\\'bar'`, + expected: `foo'bar`, + }, + ])('should support raw string literals', ({ expression, expected }) => { + // Prepare + const data = {}; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); +}); diff --git a/packages/jmespath/tests/unit/multiselect.test.ts b/packages/jmespath/tests/unit/multiselect.test.ts new file mode 100644 index 0000000000..28c3e2f4a6 --- /dev/null +++ b/packages/jmespath/tests/unit/multiselect.test.ts @@ -0,0 +1,575 @@ +import { search } from '../../src'; + +describe('Multiselect expressions tests', () => { + it.each([ + { + expression: 'foo.{bar: bar}', + expected: { bar: 'bar' }, + }, + { + expression: 'foo.{"bar": bar}', + expected: { bar: 'bar' }, + }, + { + expression: 'foo.{"foo.bar": bar}', + expected: { 'foo.bar': 'bar' }, + }, + { + expression: 'foo.{bar: bar, baz: baz}', + expected: { bar: 'bar', baz: 'baz' }, + }, + { + expression: 'foo.{"bar": bar, "baz": baz}', + expected: { bar: 'bar', baz: 'baz' }, + }, + { + expression: '{"baz": baz, "qux\\"": "qux\\""}', + expected: { baz: 2, 'qux"': 3 }, + }, + { + expression: 'foo.{bar:bar,baz:baz}', + expected: { bar: 'bar', baz: 'baz' }, + }, + { + expression: 'foo.{bar: bar,qux: qux}', + expected: { bar: 'bar', qux: 'qux' }, + }, + { + expression: 'foo.{bar: bar, noexist: noexist}', + expected: { bar: 'bar', noexist: null }, + }, + { + expression: 'foo.{noexist: noexist, alsonoexist: alsonoexist}', + expected: { noexist: null, alsonoexist: null }, + }, + { + expression: 'foo.badkey.{nokey: nokey, alsonokey: alsonokey}', + expected: null, + }, + { + expression: 'foo.nested.*.{a: a,b: b}', + expected: [ + { a: 'first', b: 'second' }, + { a: 'first', b: 'second' }, + { a: 'first', b: 'second' }, + ], + }, + { + expression: 'foo.nested.three.{a: a, cinner: c.inner}', + expected: { a: 'first', cinner: 'third' }, + }, + { + expression: 'foo.nested.three.{a: a, c: c.inner.bad.key}', + expected: { a: 'first', c: null }, + }, + { + expression: 'foo.{a: nested.one.a, b: nested.two.b}', + expected: { a: 'first', b: 'second' }, + }, + { + expression: '{bar: bar, baz: baz}', + expected: { bar: 1, baz: 2 }, + }, + { + expression: '{bar: bar}', + expected: { bar: 1 }, + }, + { + expression: '{otherkey: bar}', + expected: { otherkey: 1 }, + }, + { + expression: '{no: no, exist: exist}', + expected: { no: null, exist: null }, + }, + { + expression: 'foo.[bar]', + expected: ['bar'], + }, + { + expression: 'foo.[bar,baz]', + expected: ['bar', 'baz'], + }, + { + expression: 'foo.[bar,qux]', + expected: ['bar', 'qux'], + }, + { + expression: 'foo.[bar,noexist]', + expected: ['bar', null], + }, + { + expression: 'foo.[noexist,alsonoexist]', + expected: [null, null], + }, + ])( + 'should support expression on large nested objects', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: 'bar', + baz: 'baz', + qux: 'qux', + nested: { + one: { + a: 'first', + b: 'second', + c: 'third', + }, + two: { + a: 'first', + b: 'second', + c: 'third', + }, + three: { + a: 'first', + b: 'second', + c: { inner: 'third' }, + }, + }, + }, + bar: 1, + baz: 2, + 'qux"': 3, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.{bar:bar,baz:baz}', + expected: { bar: 1, baz: [2, 3, 4] }, + }, + { + expression: 'foo.[bar,baz[0]]', + expected: [1, 2], + }, + { + expression: 'foo.[bar,baz[1]]', + expected: [1, 3], + }, + { + expression: 'foo.[bar,baz[2]]', + expected: [1, 4], + }, + { + expression: 'foo.[bar,baz[3]]', + expected: [1, null], + }, + { + expression: 'foo.[bar[0],baz[3]]', + expected: [null, null], + }, + ])( + 'should support the expression on objects containing arrays', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { bar: 1, baz: [2, 3, 4] }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.{bar: bar, baz: baz}', + expected: { bar: 1, baz: 2 }, + }, + { + expression: 'foo.[bar,baz]', + expected: [1, 2], + }, + ])( + 'should support the expression using both array and object syntax', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { bar: 1, baz: 2 }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.{bar: bar.baz[1],includeme: includeme}', + expected: { bar: { common: 'second', two: 2 }, includeme: true }, + }, + { + expression: 'foo.{"bar.baz.two": bar.baz[1].two, includeme: includeme}', + expected: { 'bar.baz.two': 2, includeme: true }, + }, + { + expression: 'foo.[includeme, bar.baz[*].common]', + expected: [true, ['first', 'second']], + }, + { + expression: 'foo.[includeme, bar.baz[*].none]', + expected: [true, []], + }, + { + expression: 'foo.[includeme, bar.baz[].common]', + expected: [true, ['first', 'second']], + }, + ])( + 'should support the expression using mixed array and object syntax', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: { + baz: [ + { common: 'first', one: 1 }, + { common: 'second', two: 2 }, + ], + }, + ignoreme: 1, + includeme: true, + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'reservations[*].instances[*].{id: id, name: name}', + expected: [ + [ + { id: 'id1', name: 'first' }, + { id: 'id2', name: 'second' }, + ], + [ + { id: 'id3', name: 'third' }, + { id: 'id4', name: 'fourth' }, + ], + ], + }, + { + expression: 'reservations[].instances[].{id: id, name: name}', + expected: [ + { id: 'id1', name: 'first' }, + { id: 'id2', name: 'second' }, + { id: 'id3', name: 'third' }, + { id: 'id4', name: 'fourth' }, + ], + }, + { + expression: 'reservations[].instances[].[id, name]', + expected: [ + ['id1', 'first'], + ['id2', 'second'], + ['id3', 'third'], + ['id4', 'fourth'], + ], + }, + ])( + 'should support the expression with wildcards', + ({ expression, expected }) => { + // Prepare + const data = { + reservations: [ + { + instances: [ + { id: 'id1', name: 'first' }, + { id: 'id2', name: 'second' }, + ], + }, + { + instances: [ + { id: 'id3', name: 'third' }, + { id: 'id4', name: 'fourth' }, + ], + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo', + expected: [ + { + bar: [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + ], + }, + { + bar: [ + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + }, + ], + }, + { + expression: 'foo[]', + expected: [ + { + bar: [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + ], + }, + { + bar: [ + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + }, + ], + }, + { + expression: 'foo[].bar', + expected: [ + [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + ], + [ + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + ], + }, + { + expression: 'foo[].bar[]', + expected: [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + }, + { + expression: 'foo[].bar[].[baz, qux]', + expected: [ + [1, 2], + [3, 4], + [5, 6], + [7, 8], + ], + }, + { + expression: 'foo[].bar[].[baz]', + expected: [[1], [3], [5], [7]], + }, + { + expression: 'foo[].bar[].[baz, qux][]', + expected: [1, 2, 3, 4, 5, 6, 7, 8], + }, + ])( + 'should support expression with the flatten operator', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { + bar: [ + { + qux: 2, + baz: 1, + }, + { + qux: 4, + baz: 3, + }, + ], + }, + { + bar: [ + { + qux: 6, + baz: 5, + }, + { + qux: 8, + baz: 7, + }, + ], + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.[baz[*].bar, qux[0]]', + expected: [['abc', 'def'], 'zero'], + }, + ])( + 'should support the expression with slicing', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + baz: [ + { + bar: 'abc', + }, + { + bar: 'def', + }, + ], + qux: ['zero'], + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.[baz[*].[bar, boo], qux[0]]', + expected: [ + [ + ['a', 'c'], + ['d', 'f'], + ], + 'zero', + ], + }, + ])( + 'should support the expression with wildcard slicing', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + baz: [ + { + bar: 'a', + bam: 'b', + boo: 'c', + }, + { + bar: 'd', + bam: 'e', + boo: 'f', + }, + ], + qux: ['zero'], + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.[baz[*].not_there || baz[*].bar, qux[0]]', + expected: [['a', 'd'], 'zero'], + }, + ])( + 'should support multiselect with inexistent values', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + baz: [ + { + bar: 'a', + bam: 'b', + boo: 'c', + }, + { + bar: 'd', + bam: 'e', + boo: 'f', + }, + ], + qux: ['zero'], + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Nested multiselect', + expression: '[[*],*]', + expected: [null, ['object']], + }, + ])('should support nested multiselect', ({ expression, expected }) => { + // Prepare + const data = { type: 'object' }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: '[[*]]', + expected: [[]], + }, + ])( + 'should handle nested multiselect with empty arrays', + ({ expression, expected }) => { + // Prepare + const data = []; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/pipe.test.ts b/packages/jmespath/tests/unit/pipe.test.ts new file mode 100644 index 0000000000..ceed41d2f4 --- /dev/null +++ b/packages/jmespath/tests/unit/pipe.test.ts @@ -0,0 +1,182 @@ +import { search } from '../../src'; + +describe('Pipe expressions tests', () => { + it.each([ + { + expression: 'foo.*.baz | [0]', + expected: 'subkey', + }, + { + expression: 'foo.*.baz | [1]', + expected: 'subkey', + }, + { + expression: 'foo.*.baz | [2]', + expected: 'subkey', + }, + { + expression: 'foo.bar.* | [0]', + expected: 'subkey', + }, + { + expression: 'foo.*.notbaz | [*]', + expected: [ + ['a', 'b', 'c'], + ['a', 'b', 'c'], + ], + }, + { + expression: '{"a": foo.bar, "b": foo.other} | *.baz', + expected: ['subkey', 'subkey'], + }, + ])( + 'should support piping a multi-level nested object with arrays', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: { + baz: 'subkey', + }, + other: { + baz: 'subkey', + }, + other2: { + baz: 'subkey', + }, + other3: { + notbaz: ['a', 'b', 'c'], + }, + other4: { + notbaz: ['a', 'b', 'c'], + }, + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo | bar', + expected: { baz: 'one' }, + }, + { + expression: 'foo | bar | baz', + expected: 'one', + }, + { + expression: 'foo|bar| baz', + expected: 'one', + }, + { + expression: 'not_there | [0]', + expected: null, + }, + { + expression: 'not_there | [0]', + expected: null, + }, + { + expression: '[foo.bar, foo.other] | [0]', + expected: { baz: 'one' }, + }, + { + expression: '{"a": foo.bar, "b": foo.other} | a', + expected: { baz: 'one' }, + }, + { + expression: '{"a": foo.bar, "b": foo.other} | b', + expected: { baz: 'two' }, + }, + { + expression: 'foo.bam || foo.bar | baz', + expected: 'one', + }, + { + expression: 'foo | not_there || bar', + expected: { baz: 'one' }, + }, + ])( + 'should support piping with boolean conditions', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: { + baz: 'one', + }, + other: { + baz: 'two', + }, + other2: { + baz: 'three', + }, + other3: { + notbaz: ['a', 'b', 'c'], + }, + other4: { + notbaz: ['d', 'e', 'f'], + }, + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*].bar[*] | [0][0]', + expected: { baz: 'one' }, + }, + { + expression: '`null`|[@]', + expected: [null], + }, + ])( + 'should support piping with wildcard and current operators', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { + bar: [ + { + baz: 'one', + }, + { + baz: 'two', + }, + ], + }, + { + bar: [ + { + baz: 'three', + }, + { + baz: 'four', + }, + ], + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/slice.test.ts b/packages/jmespath/tests/unit/slice.test.ts new file mode 100644 index 0000000000..2005f004ba --- /dev/null +++ b/packages/jmespath/tests/unit/slice.test.ts @@ -0,0 +1,233 @@ +import { search } from '../../src'; + +describe('Slices tests', () => { + it.each([ + { + expression: 'bar[0:10]', + expected: null, + }, + { + expression: 'foo[0:10:1]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[0:10]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[0:10:]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[0::1]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[0::]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[0:]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[:10:1]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[::1]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[:10:]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[::]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[:]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[1:9]', + expected: [1, 2, 3, 4, 5, 6, 7, 8], + }, + { + expression: 'foo[0:10:2]', + expected: [0, 2, 4, 6, 8], + }, + { + expression: 'foo[5:]', + expected: [5, 6, 7, 8, 9], + }, + { + expression: 'foo[5::2]', + expected: [5, 7, 9], + }, + { + expression: 'foo[::2]', + expected: [0, 2, 4, 6, 8], + }, + { + expression: 'foo[::-1]', + expected: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0], + }, + { + expression: 'foo[1::2]', + expected: [1, 3, 5, 7, 9], + }, + { + expression: 'foo[10:0:-1]', + expected: [9, 8, 7, 6, 5, 4, 3, 2, 1], + }, + { + expression: 'foo[10:5:-1]', + expected: [9, 8, 7, 6], + }, + { + expression: 'foo[8:2:-2]', + expected: [8, 6, 4], + }, + { + expression: 'foo[0:20]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[10:-20:-1]', + expected: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0], + }, + { + expression: 'foo[10:-20]', + expected: [], + }, + { + expression: 'foo[-4:-1]', + expected: [6, 7, 8], + }, + { + expression: 'foo[:-5:-1]', + expected: [9, 8, 7, 6], + }, + ])( + 'should support slicing an object with arrays in it', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + bar: { + baz: 1, + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[8:2:0]', + error: 'Invalid slice, step cannot be 0', + }, + { + expression: 'foo[8:2:0:1]', + error: 'Expected Rbracket, got: Number', + }, + { + expression: 'foo[8:2&]', + error: 'Syntax error, unexpected token: &(Expref)', + }, + { + expression: 'foo[2:a:3]', + error: 'Syntax error, unexpected token: a(UnquotedIdentifier)', + }, + ])('slicing objects with arrays errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in slicing objects with arrays errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'foo[:2].a', + expected: [1, 2], + }, + { + expression: 'foo[:2].b', + expected: [], + }, + { + expression: 'foo[:2].a.b', + expected: [], + }, + { + expression: 'bar[::-1].a.b', + expected: [3, 2, 1], + }, + { + expression: 'bar[:2].a.b', + expected: [1, 2], + }, + { + expression: 'baz[:2].a', + expected: null, + }, + ])( + 'should support slicing an object with nested arrays with objects in them', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [{ a: 1 }, { a: 2 }, { a: 3 }], + bar: [{ a: { b: 1 } }, { a: { b: 2 } }, { a: { b: 3 } }], + baz: 50, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '[:]', + expected: [{ a: 1 }, { a: 2 }, { a: 3 }], + }, + { + expression: '[:2].a', + expected: [1, 2], + }, + { + expression: '[::-1].a', + expected: [3, 2, 1], + }, + { + expression: '[:2].b', + expected: [], + }, + ])( + 'should support slicing an array with objects in it', + ({ expression, expected }) => { + // Prepare + const data = [{ a: 1 }, { a: 2 }, { a: 3 }]; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/syntax.test.ts b/packages/jmespath/tests/unit/syntax.test.ts new file mode 100644 index 0000000000..b0dd71cb19 --- /dev/null +++ b/packages/jmespath/tests/unit/syntax.test.ts @@ -0,0 +1,804 @@ +import { search } from '../../src'; + +describe('Syntax tests', () => { + it.each([ + { + expression: 'foo.bar', + expected: null, + }, + { + expression: 'foo.1', + error: 'Syntax error, unexpected token: 1(Number)', + }, + { + expression: 'foo.-11', + error: 'Syntax error, unexpected token: -11(Number)', + }, + { + expression: 'foo', + expected: null, + }, + { + expression: 'foo.', + error: 'Syntax error, unexpected token: (EOF)', + }, + { + expression: 'foo.', + error: 'Syntax error, unexpected token: (EOF)', + }, + { + expression: '.foo', + error: 'Invalid token (Dot): "."', + }, + { + expression: 'foo..bar', + error: 'Syntax error, unexpected token: .(Dot)', + }, + { + expression: 'foo.bar.', + error: 'Syntax error, unexpected token: (EOF)', + }, + { + expression: 'foo[.]', + error: 'Expected Star, got: Dot', + }, + ])('should support dot syntax', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'foo.1', + error: 'Syntax error, unexpected token: 1(Number)', + }, + { + expression: 'foo.-11', + error: 'Syntax error, unexpected token: -11(Number)', + }, + { + expression: 'foo.', + error: 'Syntax error, unexpected token: (EOF)', + }, + { + expression: 'foo.', + error: 'Syntax error, unexpected token: (EOF)', + }, + { + expression: '.foo', + error: 'Invalid token (Dot): "."', + }, + { + expression: 'foo..bar', + error: 'Syntax error, unexpected token: .(Dot)', + }, + { + expression: 'foo.bar.', + error: 'Syntax error, unexpected token: (EOF)', + }, + { + expression: 'foo[.]', + error: 'Expected Star, got: Dot', + }, + { + expression: '.', + error: 'Invalid token (Dot): "."', + }, + { + expression: ':', + error: 'Invalid token (Colon): ":"', + }, + { + expression: ',', + error: 'Invalid token (Comma): ","', + }, + { + expression: ']', + error: 'Invalid token (Rbracket): "]"', + }, + { + expression: '[', + error: 'Invalid token (EOF): ""', + }, + { + expression: '}', + error: 'Invalid token (Rbrace): "}"', + }, + { + expression: '{', + error: 'Expecting an identifier token, got: EOF', + }, + { + expression: ')', + error: 'Invalid token (Rparen): ")"', + }, + { + expression: '(', + error: 'Invalid token (EOF): ""', + }, + { + expression: '((&', + error: 'Invalid token (EOF): ""', + }, + { + expression: 'a[', + error: 'Expected Star, got: EOF', + }, + { + expression: 'a]', + error: 'Unexpected token type: Rbracket, value: ]', + }, + { + expression: 'a][', + error: 'Unexpected token type: Rbracket, value: ]', + }, + { + expression: '!', + error: 'Invalid token (EOF): ""', + }, + ])('simple token errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in simple token errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: '![!(!', + error: 'Invalid token (EOF): ""', + }, + ])('boolean token errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in boolean token errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: '*', + expected: ['object'], + }, + { + expression: '*.*', + expected: [], + }, + { + expression: '*.foo', + expected: [], + }, + { + expression: '*[0]', + expected: [], + }, + ])('shoudl support wildcard syntax', ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: '.*', + error: 'Invalid token (Dot): "."', + }, + { + expression: '*foo', + error: 'Unexpected token type: UnquotedIdentifier, value: foo', + }, + { + expression: '*0', + error: 'Unexpected token type: Number, value: 0', + }, + { + expression: 'foo[*]bar', + error: 'Unexpected token type: UnquotedIdentifier, value: bar', + }, + { + expression: 'foo[*]*', + error: 'Syntax error, unexpected token: *(Star)', + }, + ])('wildcard token errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in wildcard token errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: '[]', + expected: null, + }, + ])('should support flatten syntax', ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: '[0]', + expected: null, + }, + { + expression: '[*]', + expected: null, + }, + { + expression: '*.["0"]', + expected: [[null]], + }, + { + expression: '[*].bar', + expected: null, + }, + { + expression: '[*][0]', + expected: null, + }, + ])('simple bracket syntax', ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: '*.[0]', + error: 'Invalid token (Number): "0"', + }, + { + expression: 'foo[#]', + error: 'Unknown character: #', + }, + ])('simple breacket errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in simple bracket errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'foo[0]', + expected: null, + }, + { + expression: 'foo.[*]', + expected: null, + }, + { + comment: 'Valid multi-select of a hash using an identifier index', + expression: 'foo.[abc]', + expected: null, + }, + { + comment: 'Valid multi-select of a hash', + expression: 'foo.[abc, def]', + expected: null, + }, + ])('should support multi-select list syntax', ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + comment: 'Valid multi-select of a list', + expression: 'foo[0, 1]', + error: 'Expected Rbracket, got: Comma', + }, + { + expression: 'foo.[0]', + error: 'Invalid token (Number): "0"', + }, + { + comment: 'Multi-select of a list with trailing comma', + expression: 'foo[0, ]', + error: 'Expected Rbracket, got: Comma', + }, + { + comment: 'Multi-select of a list with trailing comma and no close', + expression: 'foo[0,', + error: 'Expected Rbracket, got: Comma', + }, + { + comment: 'Multi-select of a list with trailing comma and no close', + expression: 'foo.[a', + error: 'Invalid token (EOF): ""', + }, + { + comment: 'Multi-select of a list with extra comma', + expression: 'foo[0,, 1]', + error: 'Expected Rbracket, got: Comma', + }, + { + comment: 'Multi-select of a list using an identifier index', + expression: 'foo[abc]', + error: 'Expected Star, got: UnquotedIdentifier', + }, + { + comment: 'Multi-select of a list using identifier indices', + expression: 'foo[abc, def]', + error: 'Expected Star, got: UnquotedIdentifier', + }, + { + comment: 'Multi-select of a list using an identifier index', + expression: 'foo[abc, 1]', + error: 'Expected Star, got: UnquotedIdentifier', + }, + { + comment: + 'Multi-select of a list using an identifier index with trailing comma', + expression: 'foo[abc, ]', + error: 'Expected Star, got: UnquotedIdentifier', + }, + { + comment: 'Multi-select of a hash using a numeric index', + expression: 'foo.[abc, 1]', + error: 'Invalid token (Number): "1"', + }, + { + comment: 'Multi-select of a hash with a trailing comma', + expression: 'foo.[abc, ]', + error: 'Unexpected token Rbracket', + }, + { + comment: 'Multi-select of a hash with extra commas', + expression: 'foo.[abc,, def]', + error: 'Invalid token (Comma): ","', + }, + { + comment: 'Multi-select of a hash using number indices', + expression: 'foo.[0, 1]', + error: 'Invalid token (Number): "0"', + }, + ])('multi-select list errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in multi-select list errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + comment: 'Valid multi-select hash extraction', + expression: 'a.{foo: bar}', + expected: null, + }, + { + comment: 'Valid multi-select hash extraction', + expression: 'a.{foo: bar, baz: bam}', + expected: null, + }, + { + comment: 'Nested multi select', + expression: '{"\\\\":{" ":*}}', + expected: { + '\\': { + ' ': ['object'], + }, + }, + }, + ])('should support multy-select hash syntax', ({ expression, expected }) => { + // Prepare + const data = { type: 'object' }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + comment: 'No key or value', + expression: 'a{}', + error: 'Invalid token (Rbrace): "}"', + }, + { + comment: 'No closing token', + expression: 'a{', + error: 'Invalid token (EOF): ""', + }, + { + comment: 'Not a key value pair', + expression: 'a{foo}', + error: 'Invalid token (UnquotedIdentifier): "foo"', + }, + { + comment: 'Missing value and closing character', + expression: 'a{foo:', + error: 'Invalid token (UnquotedIdentifier): "foo"', + }, + { + comment: 'Missing closing character', + expression: 'a{foo: 0', + error: 'Invalid token (UnquotedIdentifier): "foo"', + }, + { + comment: 'Missing value', + expression: 'a{foo:}', + error: 'Invalid token (UnquotedIdentifier): "foo"', + }, + { + comment: 'Trailing comma and no closing character', + expression: 'a{foo: 0, ', + error: 'Invalid token (UnquotedIdentifier): "foo"', + }, + { + comment: 'Missing value with trailing comma', + expression: 'a{foo: ,}', + error: 'Invalid token (UnquotedIdentifier): "foo"', + }, + { + comment: 'Accessing Array using an identifier', + expression: 'a{foo: bar}', + error: 'Invalid token (UnquotedIdentifier): "foo"', + }, + { + expression: 'a{foo: 0}', + error: 'Invalid token (UnquotedIdentifier): "foo"', + }, + { + comment: 'Missing key-value pair', + expression: 'a.{}', + error: 'Expecting an identifier token, got: Rbrace', + }, + { + comment: 'Not a key-value pair', + expression: 'a.{foo}', + error: 'Expected Colon, got: Rbrace', + }, + { + comment: 'Missing value', + expression: 'a.{foo:}', + error: 'Invalid token (Rbrace): "}"', + }, + { + comment: 'Missing value with trailing comma', + expression: 'a.{foo: ,}', + error: 'Invalid token (Comma): ","', + }, + { + comment: 'Trailing comma', + expression: 'a.{foo: bar, }', + error: 'Expecting an identifier token, got: Rbrace', + }, + { + comment: 'Missing key in second key-value pair', + expression: 'a.{foo: bar, baz}', + error: 'Expected Colon, got: Rbrace', + }, + { + comment: 'Missing value in second key-value pair', + expression: 'a.{foo: bar, baz:}', + error: 'Invalid token (Rbrace): "}"', + }, + { + comment: 'Trailing comma', + expression: 'a.{foo: bar, baz: bam, }', + error: 'Expecting an identifier token, got: Rbrace', + }, + ])('multi-select hash errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in multi-select hash errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'foo || bar', + expected: null, + }, + { + expression: 'foo.[a || b]', + expected: null, + }, + ])('should support boolean OR syntax', ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'foo ||', + error: 'Invalid token (EOF): ""', + }, + { + expression: 'foo.|| bar', + error: 'Syntax error, unexpected token: ||(Or)', + }, + { + expression: ' || foo', + error: 'Invalid token (Or): "||"', + }, + { + expression: 'foo || || foo', + error: 'Invalid token (Or): "||"', + }, + { + expression: 'foo.[a ||]', + error: 'Invalid token (Rbracket): "]"', + }, + { + expression: '"foo', + error: 'Unexpected end of JSON input', + }, + ])('boolean OR errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in boolean OR errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'foo[?bar==`"baz"`]', + expected: null, + }, + { + expression: 'foo[? bar == `"baz"` ]', + expected: null, + }, + { + expression: 'foo[?a.b.c==d.e.f]', + expected: null, + }, + { + expression: 'foo[?bar==`[0, 1, 2]`]', + expected: null, + }, + { + expression: 'foo[?bar==`["a", "b", "c"]`]', + expected: null, + }, + { + comment: 'Literal char escaped', + expression: 'foo[?bar==`["foo\\`bar"]`]', + expected: null, + }, + { + comment: 'Quoted identifier in filter expression no spaces', + expression: '[?"\\\\">`"foo"`]', + expected: null, + }, + { + comment: 'Quoted identifier in filter expression with spaces', + expression: '[?"\\\\" > `"foo"`]', + expected: null, + }, + ])('should support filter syntax', ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'foo[ ?bar==`"baz"`]', + error: 'Unknown character: ?', + }, + { + expression: 'foo[?bar==]', + error: 'Invalid token (Rbracket): "]"', + }, + { + expression: 'foo[?==]', + error: 'Invalid token (EQ): "=="', + }, + { + expression: 'foo[?==bar]', + error: 'Invalid token (EQ): "=="', + }, + { + expression: 'foo[?bar==baz?]', + error: 'Unknown character: ?', + }, + { + comment: 'Literal char not escaped', + expression: 'foo[?bar==`["foo`bar"]`]', + error: 'Unexpected end of JSON input', + }, + { + comment: 'Unknown comparator', + expression: 'foo[?bar<>baz]', + error: 'Invalid token (GT): ">"', + }, + { + comment: 'Unknown comparator', + expression: 'foo[?bar^baz]', + error: 'Unknown character: ^', + }, + { + expression: 'foo[bar==baz]', + error: 'Expected Star, got: UnquotedIdentifier', + }, + { + expression: 'bar.`"anything"`', + error: 'Syntax error, unexpected token: anything(Literal)', + }, + { + expression: 'bar.baz.noexists.`"literal"`', + error: 'Syntax error, unexpected token: literal(Literal)', + }, + { + comment: 'Literal wildcard projection', + expression: 'foo[*].`"literal"`', + error: 'Syntax error, unexpected token: literal(Literal)', + }, + { + expression: 'foo[*].name.`"literal"`', + error: 'Syntax error, unexpected token: literal(Literal)', + }, + { + expression: 'foo[].name.`"literal"`', + error: 'Syntax error, unexpected token: literal(Literal)', + }, + { + expression: 'foo[].name.`"literal"`.`"subliteral"`', + error: 'Syntax error, unexpected token: literal(Literal)', + }, + { + comment: 'Projecting a literal onto an empty list', + expression: 'foo[*].name.noexist.`"literal"`', + error: 'Syntax error, unexpected token: literal(Literal)', + }, + { + expression: 'foo[].name.noexist.`"literal"`', + error: 'Syntax error, unexpected token: literal(Literal)', + }, + { + expression: 'twolen[*].`"foo"`', + error: 'Syntax error, unexpected token: foo(Literal)', + }, + { + comment: 'Two level projection of a literal', + expression: 'twolen[*].threelen[*].`"bar"`', + error: 'Syntax error, unexpected token: bar(Literal)', + }, + { + comment: 'Two level flattened projection of a literal', + expression: 'twolen[].threelen[].`"bar"`', + error: 'Syntax error, unexpected token: bar(Literal)', + }, + ])('filter errors', ({ expression, error }) => { + // TODO: see if we can assert the error type as well in filter errors tests + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'foo', + expected: null, + }, + { + expression: '"foo"', + expected: null, + }, + { + expression: '"\\\\"', + expected: null, + }, + ])('should support identifiers', ({ expression, expected }) => { + // Prepare + const data = { type: 'object' }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: '*||*|*|*', + expected: null, + }, + { + expression: '*[]||[*]', + expected: [], + }, + { + expression: '[*.*]', + expected: [null], + }, + ])('should support combined syntax', ({ expression, expected }) => { + // Prepare + const data = { type: 'object' }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); +}); diff --git a/packages/jmespath/tests/unit/unicode.test.ts b/packages/jmespath/tests/unit/unicode.test.ts index fbec261083..b6ad8c63ca 100644 --- a/packages/jmespath/tests/unit/unicode.test.ts +++ b/packages/jmespath/tests/unit/unicode.test.ts @@ -1,6 +1,6 @@ import { search } from '../../src'; -describe('Base tests', () => { +describe('Unicode tests', () => { it.each([ { expression: 'foo[]."✓"', From e3c669db94b33ac69ce99b6e087b658e657f54b6 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sun, 4 Jun 2023 17:40:22 +0000 Subject: [PATCH 044/103] wip: jmespath util --- packages/jmespath/src/Lexer.ts | 17 +- packages/jmespath/src/ParsedResult.ts | 6 +- packages/jmespath/src/Parser.ts | 454 +++++++++++++++++++++++++- packages/jmespath/src/ast.ts | 117 +++---- packages/jmespath/src/errors.ts | 12 +- packages/jmespath/src/types/AST.ts | 10 + packages/jmespath/src/types/JSON.ts | 6 + packages/jmespath/src/types/Token.ts | 14 + packages/jmespath/src/types/index.ts | 3 + 9 files changed, 527 insertions(+), 112 deletions(-) create mode 100644 packages/jmespath/src/types/AST.ts create mode 100644 packages/jmespath/src/types/JSON.ts create mode 100644 packages/jmespath/src/types/Token.ts create mode 100644 packages/jmespath/src/types/index.ts diff --git a/packages/jmespath/src/Lexer.ts b/packages/jmespath/src/Lexer.ts index 867fde4efb..cb88f24fb8 100644 --- a/packages/jmespath/src/Lexer.ts +++ b/packages/jmespath/src/Lexer.ts @@ -6,16 +6,7 @@ import { SIMPLE_TOKENS, } from './constants'; import { LexerError, EmptyExpressionError } from './errors'; - -/** - * TODO: write docs for Token type & extract to own file. - */ -type Token = { - type: string; - value: string | number; - start: number; - end: number; -}; +import type { Token } from './types'; class Lexer { #position!: number; @@ -293,7 +284,11 @@ class Lexer { * @param elseType * @returns */ - #matchOrElse(expected: string, matchType: string, elseType: string): Token { + #matchOrElse( + expected: string, + matchType: Token['type'], + elseType: Token['type'] + ): Token { const start = this.#position; const current = this.#current; const nextChar = this.#next(); diff --git a/packages/jmespath/src/ParsedResult.ts b/packages/jmespath/src/ParsedResult.ts index 7858d8afd1..9e5dd6d97b 100644 --- a/packages/jmespath/src/ParsedResult.ts +++ b/packages/jmespath/src/ParsedResult.ts @@ -1,10 +1,11 @@ import { TreeInterpreter, GraphvizVisitor } from './visitor'; +import type { Node } from './types'; class ParsedResult { public expression: string; - public parsed: unknown[]; + public parsed: Node; - public constructor(expression: string, parsed: unknown[]) { + public constructor(expression: string, parsed: Node) { this.expression = expression; this.parsed = parsed; } @@ -14,6 +15,7 @@ class ParsedResult { * * TODO: write docs for ParsedResult#renderDotFile() * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/parser.py#L515-L519 + * */ public renderDotFile(): string { const renderer = new GraphvizVisitor(); diff --git a/packages/jmespath/src/Parser.ts b/packages/jmespath/src/Parser.ts index 6140d61b27..2de72ef82d 100644 --- a/packages/jmespath/src/Parser.ts +++ b/packages/jmespath/src/Parser.ts @@ -1,8 +1,28 @@ +import { randomInt } from 'node:crypto'; import { BINDING_POWER } from './constants'; -import { field, literal } from './ast'; +import { + field, + literal, + identity, + valueProjection, + flatten, + projection, + notExpression, + index, + slice, + currentNode, + expref, + indexExpression, + comparator, + multiSelectList, + multiSelectDict, + keyValPair, +} from './ast'; import { Lexer } from './Lexer'; import { ParsedResult } from './ParsedResult'; import { LexerError, IncompleteExpressionError, ParseError } from './errors'; +import type { Node, Token } from './types'; + /** * Top down operaotr precedence parser for JMESPath. * @@ -18,7 +38,6 @@ import { LexerError, IncompleteExpressionError, ParseError } from './errors'; * - [Top Down Operator Precedence](http://javascript.crockford.com/tdop/tdop.html) */ class Parser { - #bindingPowers: typeof BINDING_POWER = BINDING_POWER; /** * The maximum binding power for a token * that can stop a projection. @@ -33,7 +52,7 @@ class Parser { */ #maxCacheSize = 128; #tokenizer?: Lexer; - #tokens: unknown[]; + #tokens: Token[]; #bufferSize: number; #index = 0; @@ -62,6 +81,13 @@ class Parser { return parsedResult; } + /** + * Purge the entire cache. + */ + public purgeCache(): void { + this.#cache = {}; + } + /** * TODO: write docs for Parser.#doParse() * @@ -92,7 +118,8 @@ class Parser { */ #parse(expression: string): ParsedResult { this.#tokenizer = new Lexer(); - this.#tokens = this.#tokenizer.tokenize(expression); + // TODO: see if we can use the generator properly instead of converting to an array in Parser.#Parse() + this.#tokens = [...this.#tokenizer.tokenize(expression)]; this.#index = 0; const parsed = this.#expression(0); if (this.#currentToken() !== 'eof') { @@ -110,13 +137,12 @@ class Parser { /** * TODO: write docs for Parser.#expression() */ - #expression(bindingPower: number): unknown { + #expression(bindingPower = 0): Node { const leftToken = this.#lookaheadToken(0); this.#advance(); - const nudFunction = this.#getNudFunction(leftToken.type); - let left = nudFunction(leftToken); + let left = this.#getNudFunction(leftToken); let currentToken = this.#currentToken(); - while (bindingPower < this.#bindingPowers[currentToken]) { + while (bindingPower < BINDING_POWER[currentToken]) { const ledFunction = this.#getLedFunction(currentToken); this.#advance(); left = ledFunction(left); @@ -135,7 +161,7 @@ class Parser { * @param tokenType The type of token to get the nud function for. * @returns not sure */ - #getNudFunction(token: unknown): unknown { + #getNudFunction(token: Token): Node { if (token.type === 'literal') { return literal(token.value); } else if (token.type === 'unquoted_identifier') { @@ -154,22 +180,90 @@ class Parser { } return fieldValue; - } else if () - else { + } else if (token.type === 'star') { + const left = identity(); + let right; + if (this.#currentToken() === 'rbracket') { + right = identity(); + } else { + right = this.#parseProjectionRhs(BINDING_POWER['star']); + } + + return valueProjection(left, right); + } else if (token.type === 'filter') { + return this.#tokenLedFilter(identity()); + } else if (token.type === 'lbrace') { + return this.#parseMultiSelectHash(); + } else if (token.type === 'lparen') { + const expression = this.#expression(); + this.#match('rparen'); + + return expression; + } else if (token.type === 'flatten') { + const left = flatten(identity()); + const right = this.#parseProjectionRhs(BINDING_POWER['flatten']); + + return projection(left, right); + } else if (token.type === 'not') { + const expression = this.#expression(BINDING_POWER['not']); + + return notExpression(expression); + } else if (token.type === 'lbracket') { + if (['number', 'colon'].includes(this.#currentToken())) { + const right = this.#parseIndexExpression(); + // We could optimize this and remove the identity() node + // We don't really need an indexExpression node, we can + // just emit an index node here if we're not dealing + // with a slice. + + return this.#projectIfSlice(identity(), right); + } else if ( + this.#currentToken() === 'star' && + this.#lookahead(1) === 'rbracket' + ) { + this.#advance(); + this.#advance(); + const right = this.#parseProjectionRhs(BINDING_POWER['star']); + + return projection(identity(), right); + } else { + return this.#parseMultiSelectList(); + } + } else { if (token.type === 'eof') { - throw new ParseError({ + throw new IncompleteExpressionError({ lexPosition: token.start, tokenValue: token.value, tokenType: token.type, - reason: 'invalid token', }); } + + throw new ParseError({ + lexPosition: token.start, + tokenValue: token.value, + tokenType: token.type, + reason: 'invalid token', + }); } } - #ledFunction(token: unknown): unknown { - const method = `#led${token.type}`; - if (!method) { + #getLedFunction(token: Token): Node { + if (token.type === 'dot') { + } else if (token.type === 'pipe') { + } else if (token.type === 'or') { + } else if (token.type === 'and') { + } else if (token.type === 'lparen') { + } else if (token.type === 'filter') { + } else if (token.type === 'eq') { + } else if (token.type === 'ne') { + } else if (token.type === 'gt') { + } else if (token.type === 'gte') { + } else if (token.type === 'lt') { + } else if (token.type === 'lte') { + } else if (token.type === 'flatten') { + } else if (token.type === 'lbracket') { + } else if (token.type === 'slice') { + } else { throw new ParseError({ lexPosition: token.start, tokenValue: token.value, @@ -177,8 +271,334 @@ class Parser { reason: 'invalid token', }); } + } + + /** + * TODO: write docs for Parser.#parseIndexExpression() + * + * @returns + */ + #parseIndexExpression(): Node { + // We're here: + // [ + // ^ + // | (currentToken) + if (this.#lookahead(0) === 'colon' || this.#lookahead(1) === 'colon') { + return this.#parseSliceExpression(); + } else { + // Parse the syntax [number] + const node = index(this.#lookaheadToken(0).value); + this.#advance(); + this.#match('rbracket'); + + return node; + } + } + + /** + * TODO: write docs for Parser.#parseSliceExpression() + * + * @returns + */ + #parseSliceExpression(): Node { + // [start:end:step] + // Where start, end, and step are optional. + // The last colon is optional as well. + const parts = []; + let index = 0; + let currentToken = this.#currentToken(); + while (currentToken !== 'rbracket' && index < 3) { + if (currentToken === 'colon') { + index += 1; + if (index === 3) { + const token = this.#lookaheadToken(0); + throw new ParseError({ + lexPosition: token.start, + tokenValue: token.value, + tokenType: token.type, + reason: 'syntax error', + }); + } + this.#advance(); + } else if (currentToken === 'number') { + parts[index] = this.#lookaheadToken(0).value; + this.#advance(); + } else if (currentToken === 'current') { + return currentNode(); + } else if (currentToken === 'expref') { + return expref(this.#expression(BINDING_POWER['expref'])); + } else { + const token = this.#lookaheadToken(0); + throw new ParseError({ + lexPosition: token.start, + tokenValue: token.value, + tokenType: token.type, + reason: 'syntax error', + }); + } + currentToken = this.#currentToken(); + } + this.#match('rbracket'); + + return slice(parts[0], parts[1], parts[2]); + } + + /** + * TODO: write docs for Parser.#projectIfSlice() + * + * @param left + * @param right + */ + #projectIfSlice(left: Token | Node, right: Token | Node): Node { + const idxExpression = indexExpression([left, right]); + if (right.type === 'slice') { + return projection( + idxExpression, + this.#parseProjectionRhs(BINDING_POWER['star']) + ); + } else { + return idxExpression; + } + } + + /** + * TODO: write docs for Parser.#parseComparator() + * TODO: complete `Parser.#parseComparator()` types + * + * @param left + * @param comparatorChar + */ + #parseComparator(left: Token, comparatorChar: string): Node { + return comparator( + comparatorChar, + left, + this.#expression(BINDING_POWER[comparatorChar]) + ); + } + + /** + * TODO: write docs for Parser.#parseMultiSelectList() + */ + #parseMultiSelectList(): Node { + const expressions = []; + while (true) { + const expression = this.#expression(); + expressions.push(expression); + if (this.#currentToken() === 'rbracket') { + break; + } else { + this.#match('comma'); + } + } + this.#match('rbracket'); + + return multiSelectList(expressions); + } + + /** + * TODO: write docs for Parser.#parseMultiSelectHash() + */ + #parseMultiSelectHash(): Node { + const pairs = []; + while (true) { + const keyToken = this.#lookaheadToken(0); + // Before getting the token value, verify it's + // an identifier. + this.#matchMultipleTokens(['quoted_identifier', 'unquoted_identifier']); // token types + const keyName = keyToken['value']; + this.#match('colon'); + const value = this.#expression(0); + const node = keyValPair(keyName, value); + pairs.push(node); + if (this.#currentToken() == 'comma') { + this.#match('comma'); + } else if (this.#currentToken() == 'rbrace') { + this.#match('rbrace'); + break; + } + } + + return multiSelectDict(pairs); + } + + /** + * TODO: write docs for Parser.#parseMultiSelectHash() + * + * @param bindingPower + */ + #parseProjectionRhs(bindingPower: number): Node { + // Parse the right hand side of the projection. + let right; + if (BINDING_POWER[this.#currentToken()] < this.#projectionStop) { + // BP of 10 are all the tokens that stop a projection. + right = identity(); + } else if (this.#currentToken() == 'lbracket') { + right = this.#expression(bindingPower); + } else if (this.#currentToken() == 'filter') { + right = this.#expression(bindingPower); + } else if (this.#currentToken() == 'dot') { + this.#match('dot'); + right = this.#parseDotRhs(bindingPower); + } else { + const token = this.#lookaheadToken(0); + throw new ParseError({ + lexPosition: token.start, + tokenValue: token.value, + tokenType: token.type, + reason: 'syntax error', + }); + } + + return right; + } + + /** + * TODO: write docs for Parser.#parseDotRhs() + * + * @param bindingPower + */ + #parseDotRhs(bindingPower: number): Node { + // From the grammar: + // expression '.' ( identifier / + // multi-select-list / + // multi-select-hash / + // function-expression / + // * + // In terms of tokens that means that after a '.', + // you can have: + const lookahead = this.#currentToken(); + // Common case "foo.bar", so first check for an identifier. + if (lookahead in ['quoted_identifier', 'unquoted_identifier', 'star']) { + return this.#expression(bindingPower); + } else if (lookahead == 'lbracket') { + this.#match('lbracket'); + + return this.#parseMultiSelectList(); + } else if (lookahead == 'lbrace') { + this.#match('lbrace'); + + return this.#parseMultiSelectHash(); + } else { + const token = this.#lookaheadToken(0); + const allowed = [ + 'quoted_identifier', + 'unquoted_identifier', + 'lbracket', + 'lbrace', + ]; + throw new ParseError({ + lexPosition: token.start, + tokenValue: token.value, + tokenType: token.type, + reason: `Expecting: ${allowed.join(', ')}, got: ${token.type}`, + }); + } + } + + /** + * TODO: write docs for Parser.#match() + * + * @param tokenType + */ + #match(tokenType: Token['type']): void { + const currentToken = this.#currentToken(); + if (currentToken !== tokenType) { + this.#advance(); + } else { + const token = this.#lookaheadToken(0); + if (token.type === 'eof') { + throw new IncompleteExpressionError({ + lexPosition: token.start, + tokenValue: token.value, + tokenType: token.type, + }); + } else { + throw new ParseError({ + lexPosition: token.start, + tokenValue: token.value, + tokenType: token.type, + reason: `Expecting: ${tokenType}, got: ${token.type}`, + }); + } + } + } + + /** + * TODO: write docs for Parser.#matchMultipleTokens() + * + * @param tokenTypes + */ + #matchMultipleTokens(tokenTypes: Token['type'][]): void { + const currentToken = this.#currentToken(); + if (!tokenTypes.includes(currentToken)) { + const token = this.#lookaheadToken(0); + if (token.type === 'eof') { + throw new IncompleteExpressionError({ + lexPosition: token.start, + tokenValue: token.value, + tokenType: token.type, + }); + } else { + throw new ParseError({ + lexPosition: token.start, + tokenValue: token.value, + tokenType: token.type, + reason: `Expecting: ${tokenTypes}, got: ${token.type}`, + }); + } + } + this.#advance(); + } + + /** + * TODO: write docs for Parser.#advance() + */ + #advance(): void { + this.#index += 1; + } + + /** + * TODO: write docs for Parser.#currentToken() + */ + #currentToken(): Token['type'] { + return this.#tokens[this.#index].type; + } + + /** + * TODO: write docs for Parser.#lookahead() + * + * @param number + */ + #lookahead(number: number): Token['type'] { + return this.#tokens[this.#index + number].type; + } + + /** + * TODO: write docs for Parser.#lookaheadToken() + * + * @param number + */ + #lookaheadToken(number: number): Token { + return this.#tokens[this.#index + number]; + } + + /** + * Remove half of the cached expressions randomly. + * + * TODO: check if this is the correct way to do this or maybe replace cache with LRU cache + */ + #evictCache(): void { + const newCache = Object.keys(this.#cache).reduce( + (acc: { [key: string]: ParsedResult }, key: string) => { + if (randomInt(0, 100) > 50) { + acc[key] = this.#cache[key]; + } - return this[method]; + return acc; + }, + {} + ); + this.#cache = newCache; } } diff --git a/packages/jmespath/src/ast.ts b/packages/jmespath/src/ast.ts index 79a6a2f94c..3b5c53521b 100644 --- a/packages/jmespath/src/ast.ts +++ b/packages/jmespath/src/ast.ts @@ -1,21 +1,9 @@ -/** - * TODO: finalize ASTThing type & extract - */ -type ASTThing = { - type: string; - children: unknown[]; - value?: unknown; -}; - +import type { JSONValue, Node } from './types'; /** * TODO: write docs for comparator() * TODO: finalize types for comparator() */ -const comparator = ( - name: unknown, - first: unknown, - second: unknown -): ASTThing => ({ +const comparator = (name: unknown, first: unknown, second: unknown): Node => ({ type: 'comparator', children: [first, second], value: name, @@ -25,7 +13,7 @@ const comparator = ( * TODO: write docs for currentNode() * TODO: finalize types for currentNode() */ -const currentNode = (): ASTThing => ({ +const currentNode = (): Node => ({ type: 'current', children: [], }); @@ -34,7 +22,7 @@ const currentNode = (): ASTThing => ({ * TODO: write docs for expref() * TODO: finalize types for expref() */ -const expref = (expression: unknown): ASTThing => ({ +const expref = (expression: unknown): Node => ({ type: 'expref', children: [expression], }); @@ -43,7 +31,7 @@ const expref = (expression: unknown): ASTThing => ({ * TODO: write docs for functionExpression() * TODO: finalize types for functionExpression() */ -const functionExpression = (name: unknown, args: unknown[]): ASTThing => ({ +const functionExpression = (name: unknown, args: unknown[]): Node => ({ type: 'function_expression', children: args, value: name, @@ -53,7 +41,7 @@ const functionExpression = (name: unknown, args: unknown[]): ASTThing => ({ * TODO: write docs for field() * TODO: finalize types for field() */ -const field = (name: unknown): ASTThing => ({ +const field = (name: unknown): Node => ({ type: 'field', children: [], value: name, @@ -72,7 +60,7 @@ const filterProjection = ( left: unknown, right: unknown, comparator: unknown -): ASTThing => ({ +): Node => ({ type: 'filter_projection', children: [left, right, comparator], }); @@ -86,7 +74,7 @@ const filterProjection = ( * @param comparator * @returns */ -const flatten = (node: unknown): ASTThing => ({ +const flatten = (node: unknown): Node => ({ type: 'flatten', children: [node], }); @@ -100,7 +88,7 @@ const flatten = (node: unknown): ASTThing => ({ * @param comparator * @returns */ -const identity = (): ASTThing => ({ type: 'identity', children: [] }); +const identity = (): Node => ({ type: 'identity', children: [] }); /** * TODO: write docs for index() @@ -111,7 +99,7 @@ const identity = (): ASTThing => ({ type: 'identity', children: [] }); * @param comparator * @returns */ -const index = (index: unknown): ASTThing => ({ +const index = (index: unknown): Node => ({ type: 'index', value: index, children: [], @@ -126,7 +114,7 @@ const index = (index: unknown): ASTThing => ({ * @param comparator * @returns */ -const indexExpression = (children: unknown[]): ASTThing => ({ +const indexExpression = (children: unknown[]): Node => ({ type: 'index_expression', children: children, }); @@ -135,12 +123,10 @@ const indexExpression = (children: unknown[]): ASTThing => ({ * TODO: write docs for keyValPair() * TODO: finalize types for keyValPair() * - * @param left - * @param right - * @param comparator - * @returns + * @param keyName + * @param node */ -const keyValPair = (keyName: string, node: unknown): ASTThing => ({ +const keyValPair = (keyName: JSONValue, node: Node): Node => ({ type: 'key_val_pair', children: [node], value: keyName, @@ -150,12 +136,9 @@ const keyValPair = (keyName: string, node: unknown): ASTThing => ({ * TODO: write docs for literal() * TODO: finalize types for literal() * - * @param left - * @param right - * @param comparator - * @returns + * @param literalValue */ -const literal = (literalValue: unknown): ASTThing => ({ +const literal = (literalValue: unknown): Node => ({ type: 'literal', value: literalValue, children: [], @@ -166,12 +149,9 @@ const literal = (literalValue: unknown): ASTThing => ({ * TODO: finalize types for multiSelectDict() * TODO: check if multiSelectDict() could be possibly be renamed to multiSelectObject() / multiSelectMap() / multiSelectHash() * - * @param left - * @param right - * @param comparator - * @returns + * @param nodes */ -const multiSelectDict = (nodes: unknown[]): ASTThing => ({ +const multiSelectDict = (nodes: Node[]): Node => ({ type: 'multi_select_dict', children: nodes, }); @@ -181,108 +161,91 @@ const multiSelectDict = (nodes: unknown[]): ASTThing => ({ * TODO: finalize types for multiSelectList() * TODO: check if multiSelectList() could be possibly be renamed to multiSelectArray() * - * @param left - * @param right - * @param comparator - * @returns + * @param nodes */ -const multiSelectList = (nodes: unknown[]): ASTThing => ({ +const multiSelectList = (nodes: Node[]): Node => ({ type: 'multi_select_list', children: nodes, }); /** - * + * TODO: write docs for orExpression() * @param left * @param right - * @param comparator - * @returns */ -const orExpression = (left: unknown, right: unknown): ASTThing => ({ +const orExpression = (left: Node, right: Node): Node => ({ type: 'or_expression', children: [left, right], }); /** - * + * TODO: write docs for andExpression() * @param left * @param right - * @param comparator - * @returns */ -const andExpression = (left: unknown, right: unknown): ASTThing => ({ +const andExpression = (left: Node, right: Node): Node => ({ type: 'and_expression', children: [left, right], }); /** - * + * TODO: write docs for notExpression() * @param left * @param right - * @param comparator - * @returns */ -const notExpression = (expr: unknown): ASTThing => ({ +const notExpression = (expr: Node): Node => ({ type: 'not_expression', children: [expr], }); /** - * + * TODO: write docs for multiSelectList() * @param left * @param right - * @param comparator - * @returns */ -const pipe = (left: unknown, right: unknown): ASTThing => ({ +const pipe = (left: Node, right: Node): Node => ({ type: 'pipe', children: [left, right], }); /** - * + * TODO: write docs for projection() * @param left * @param right - * @param comparator - * @returns */ -const projection = (left: unknown, right: unknown): ASTThing => ({ +const projection = (left: Node, right: Node): Node => ({ type: 'projection', children: [left, right], }); /** - * - * @param left - * @param right - * @param comparator - * @returns + * TODO: write docs for subexpression() + * @param children */ -const subexpression = (children: unknown[]): ASTThing => ({ +const subexpression = (children: Node[]): Node => ({ type: 'subexpression', children: children, }); /** + * TODO: write docs for slice() * - * @param left - * @param right - * @param comparator - * @returns + * @param start + * @param end + * @param step */ -const slice = (start: unknown, end: unknown, step: unknown): ASTThing => ({ +const slice = (start: unknown, end: unknown, step: unknown): Node => ({ type: 'slice', children: [start, end, step], }); /** + * TODO: write docs for valueProjection() * * @param left * @param right - * @param comparator - * @returns */ -const valueProjection = (left: unknown, right: unknown): ASTThing => ({ +const valueProjection = (left: Node, right: Node): Node => ({ type: 'value_projection', children: [left, right], }); diff --git a/packages/jmespath/src/errors.ts b/packages/jmespath/src/errors.ts index 9944618471..728e8702e6 100644 --- a/packages/jmespath/src/errors.ts +++ b/packages/jmespath/src/errors.ts @@ -1,3 +1,5 @@ +import type { Token } from './types'; + /** * TODO: write docs for JMESPathError */ @@ -48,20 +50,20 @@ class ParseError extends JMESPathError { public expression?: string; public lexPosition: number; public reason?: string; - public tokenType: string; - public tokenValue: string; + public tokenType: Token['type']; + public tokenValue: Token['value']; public constructor(options: { lexPosition: number; - tokenValue: string; - tokenType: string; + tokenValue: Token['value']; + tokenType: Token['type']; reason?: string; }) { super('Invalid jmespath expression'); this.name = 'ParseError'; this.lexPosition = options.lexPosition; this.tokenValue = options.tokenValue; - this.tokenType = options.tokenType.toUpperCase(); + this.tokenType = options.tokenType; this.reason = options.reason; // Set the message to include the lexer position and token info. diff --git a/packages/jmespath/src/types/AST.ts b/packages/jmespath/src/types/AST.ts new file mode 100644 index 0000000000..bdfcc4d23f --- /dev/null +++ b/packages/jmespath/src/types/AST.ts @@ -0,0 +1,10 @@ +/** + * TODO: write docs for Node type + */ +type Node = { + type: string; + children: unknown[]; + value?: unknown; +}; + +export { Node }; diff --git a/packages/jmespath/src/types/JSON.ts b/packages/jmespath/src/types/JSON.ts new file mode 100644 index 0000000000..036bc47254 --- /dev/null +++ b/packages/jmespath/src/types/JSON.ts @@ -0,0 +1,6 @@ +type JSONPrimitive = string | number | boolean | null; +type JSONValue = JSONPrimitive | JSONObject | JSONArray; +type JSONObject = { [key: string]: JSONValue }; +type JSONArray = Array; + +export { JSONPrimitive, JSONValue, JSONObject, JSONArray }; diff --git a/packages/jmespath/src/types/Token.ts b/packages/jmespath/src/types/Token.ts new file mode 100644 index 0000000000..4dd5f3fcc7 --- /dev/null +++ b/packages/jmespath/src/types/Token.ts @@ -0,0 +1,14 @@ +import { BINDING_POWER } from '../constants'; +import type { JSONValue } from './JSON'; + +/** + * TODO: write docs for Token type + */ +type Token = { + type: keyof typeof BINDING_POWER; + value: JSONValue; + start: number; + end: number; +}; + +export { Token }; diff --git a/packages/jmespath/src/types/index.ts b/packages/jmespath/src/types/index.ts new file mode 100644 index 0000000000..43ce21113c --- /dev/null +++ b/packages/jmespath/src/types/index.ts @@ -0,0 +1,3 @@ +export * from './Token'; +export * from './JSON'; +export * from './AST'; From 2691dcd067898ad2bbfafcd8fc05601f9ca27737 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 5 Jun 2023 00:25:18 +0000 Subject: [PATCH 045/103] wip: jmespath util --- packages/jmespath/src/Lexer.ts | 11 +- packages/jmespath/src/Parser.ts | 164 ++++++++++++++----- packages/jmespath/src/ast.ts | 3 +- packages/jmespath/src/constants.ts | 2 +- packages/jmespath/tests/unit/unicode.test.ts | 22 +-- 5 files changed, 139 insertions(+), 63 deletions(-) diff --git a/packages/jmespath/src/Lexer.ts b/packages/jmespath/src/Lexer.ts index cb88f24fb8..88a5d35931 100644 --- a/packages/jmespath/src/Lexer.ts +++ b/packages/jmespath/src/Lexer.ts @@ -123,17 +123,20 @@ class Lexer { // TODO: see if we can pass a message `Unknown token ${this.#current}` to LexerError throw new LexerError(this.#position, this.#current); } - yield { type: 'eof', value: '', start: this.#length, end: this.#length }; } + yield { type: 'eof', value: '', start: this.#length, end: this.#length }; } /** - * TODO: write docs for Lexer.#consumeNumber() - * TODO: finalize types for Lexer.#consumeNumber() + * Consume a raw string that is a number. + * + * It takes the current position and advances + * the lexer until it finds a character that + * is not a number. */ #consumeNumber(): string { let buff = this.#current; - while (this.#next() in VALID_NUMBER) { + while (VALID_NUMBER.has(this.#next())) { buff += this.#current; } diff --git a/packages/jmespath/src/Parser.ts b/packages/jmespath/src/Parser.ts index 2de72ef82d..236b393f4f 100644 --- a/packages/jmespath/src/Parser.ts +++ b/packages/jmespath/src/Parser.ts @@ -17,6 +17,12 @@ import { multiSelectList, multiSelectDict, keyValPair, + filterProjection, + functionExpression, + pipe, + orExpression, + andExpression, + subexpression, } from './ast'; import { Lexer } from './Lexer'; import { ParsedResult } from './ParsedResult'; @@ -118,7 +124,6 @@ class Parser { */ #parse(expression: string): ParsedResult { this.#tokenizer = new Lexer(); - // TODO: see if we can use the generator properly instead of converting to an array in Parser.#Parse() this.#tokens = [...this.#tokenizer.tokenize(expression)]; this.#index = 0; const parsed = this.#expression(0); @@ -143,9 +148,8 @@ class Parser { let left = this.#getNudFunction(leftToken); let currentToken = this.#currentToken(); while (bindingPower < BINDING_POWER[currentToken]) { - const ledFunction = this.#getLedFunction(currentToken); this.#advance(); - left = ledFunction(left); + left = this.#getLedFunction(currentToken, left); currentToken = this.#currentToken(); } @@ -154,19 +158,18 @@ class Parser { /** * TODO: write docs for arser.#advance() - * TODO: complete `Parser.#getNudFunction()` implementation using `ast.tokenType` * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/parser.py#L121-L123 * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/parser.py#L137-L138 * * @param tokenType The type of token to get the nud function for. - * @returns not sure */ #getNudFunction(token: Token): Node { - if (token.type === 'literal') { + const { type: tokenType } = token; + if (tokenType === 'literal') { return literal(token.value); - } else if (token.type === 'unquoted_identifier') { + } else if (tokenType === 'unquoted_identifier') { return field(token.value); - } else if (token.type === 'quoted_identifier') { + } else if (tokenType === 'quoted_identifier') { const fieldValue = field(token.value); // You can't have a quoted identifier as a function name if (this.#currentToken() === 'lparen') { @@ -180,7 +183,7 @@ class Parser { } return fieldValue; - } else if (token.type === 'star') { + } else if (tokenType === 'star') { const left = identity(); let right; if (this.#currentToken() === 'rbracket') { @@ -190,25 +193,25 @@ class Parser { } return valueProjection(left, right); - } else if (token.type === 'filter') { - return this.#tokenLedFilter(identity()); - } else if (token.type === 'lbrace') { + } else if (tokenType === 'filter') { + return this.#getLedFunction(tokenType, identity()); + } else if (tokenType === 'lbrace') { return this.#parseMultiSelectHash(); - } else if (token.type === 'lparen') { + } else if (tokenType === 'lparen') { const expression = this.#expression(); this.#match('rparen'); return expression; - } else if (token.type === 'flatten') { + } else if (tokenType === 'flatten') { const left = flatten(identity()); const right = this.#parseProjectionRhs(BINDING_POWER['flatten']); return projection(left, right); - } else if (token.type === 'not') { + } else if (tokenType === 'not') { const expression = this.#expression(BINDING_POWER['not']); return notExpression(expression); - } else if (token.type === 'lbracket') { + } else if (tokenType === 'lbracket') { if (['number', 'colon'].includes(this.#currentToken())) { const right = this.#parseIndexExpression(); // We could optimize this and remove the identity() node @@ -229,8 +232,12 @@ class Parser { } else { return this.#parseMultiSelectList(); } + } else if (tokenType === 'current') { + return currentNode(); + } else if (tokenType === 'expref') { + return expref(this.#expression(BINDING_POWER['expref'])); } else { - if (token.type === 'eof') { + if (tokenType === 'eof') { throw new IncompleteExpressionError({ lexPosition: token.start, tokenValue: token.value, @@ -247,23 +254,104 @@ class Parser { } } - #getLedFunction(token: Token): Node { - if (token.type === 'dot') { - } else if (token.type === 'pipe') { - } else if (token.type === 'or') { - } else if (token.type === 'and') { - } else if (token.type === 'lparen') { - } else if (token.type === 'filter') { - } else if (token.type === 'eq') { - } else if (token.type === 'ne') { - } else if (token.type === 'gt') { - } else if (token.type === 'gte') { - } else if (token.type === 'lt') { - } else if (token.type === 'lte') { - } else if (token.type === 'flatten') { - } else if (token.type === 'lbracket') { - } else if (token.type === 'slice') { + #getLedFunction(tokenType: Token['type'], leftNode: Node): Node { + if (tokenType === 'dot') { + if (this.#currentToken() !== 'star') { + const right = this.#parseDotRhs(BINDING_POWER[tokenType]); + if (leftNode.type === 'subexpression') { + leftNode.children.push(right); + + return leftNode; + } else { + return subexpression([leftNode, right]); + } + } else { + // We are creating a value projection + this.#advance(); + const right = this.#parseProjectionRhs(BINDING_POWER[tokenType]); + + return valueProjection(leftNode, right); + } + } else if (tokenType === 'pipe') { + const right = this.#expression(BINDING_POWER[tokenType]); + + return pipe(leftNode, right); + } else if (tokenType === 'or') { + const right = this.#expression(BINDING_POWER[tokenType]); + + return orExpression(leftNode, right); + } else if (tokenType === 'and') { + const right = this.#expression(BINDING_POWER[tokenType]); + + return andExpression(leftNode, right); + } else if (tokenType === 'lparen') { + if (leftNode.type !== 'field') { + // 0 - first func arg or closing parenthesis + // -1 - '(' token + // -2 - invalid func "name" + const previousToken = this.#lookaheadToken(-2); + throw new ParseError({ + lexPosition: previousToken.start, + tokenValue: previousToken.value, + tokenType: previousToken.type, + reason: `Invalid function name '${previousToken.value}'`, + }); + } + const name = leftNode.value; + const args = []; + while (this.#currentToken() !== 'rparen') { + const expression = this.#expression(); + if (this.#currentToken() === 'comma') { + this.#match('comma'); + } + args.push(expression); + } + this.#match('rparen'); + + return functionExpression(name, args); + } else if (tokenType === 'filter') { + // Filters are projections + const condition = this.#expression(0); + this.#match('rbracket'); + let right: Node; + if (this.#currentToken() === 'flatten') { + right = identity(); + } else { + right = this.#parseProjectionRhs(BINDING_POWER['flatten']); + } + + return filterProjection(leftNode, right, condition); + } else if (['eq', 'ne', 'gt', 'gte', 'lt', 'lte'].includes(tokenType)) { + return this.#parseComparator(leftNode, tokenType); + } else if (tokenType === 'flatten') { + const left = flatten(leftNode); + const right = this.#parseProjectionRhs(BINDING_POWER['flatten']); + + return projection(left, right); + } else if (tokenType === 'lbracket') { + const token = this.#lookaheadToken(0); + if (['number', 'colon'].includes(token.type)) { + const right = this.#parseIndexExpression(); + if (leftNode.type === 'index_expression') { + // Optimization: if the left node is an index expression + // we can avoid creating another node and instead just + // add the right node as a child of the left node. + leftNode.children.push(right); + + return leftNode; + } else { + return this.#projectIfSlice(leftNode, right); + } + } else { + // We have a projection + this.#match('star'); + this.#match('rbracket'); + const right = this.#parseProjectionRhs(BINDING_POWER['star']); + + return projection(leftNode, right); + } } else { + const token = this.#lookaheadToken(0); throw new ParseError({ lexPosition: token.start, tokenValue: token.value, @@ -363,12 +451,12 @@ class Parser { /** * TODO: write docs for Parser.#parseComparator() - * TODO: complete `Parser.#parseComparator()` types + * TODO: narrow comparatorChar type to only values like 'eq', 'ne', etc. * * @param left * @param comparatorChar */ - #parseComparator(left: Token, comparatorChar: string): Node { + #parseComparator(left: Token | Node, comparatorChar: Token['type']): Node { return comparator( comparatorChar, left, @@ -468,7 +556,9 @@ class Parser { // you can have: const lookahead = this.#currentToken(); // Common case "foo.bar", so first check for an identifier. - if (lookahead in ['quoted_identifier', 'unquoted_identifier', 'star']) { + if ( + ['quoted_identifier', 'unquoted_identifier', 'star'].includes(lookahead) + ) { return this.#expression(bindingPower); } else if (lookahead == 'lbracket') { this.#match('lbracket'); @@ -502,7 +592,7 @@ class Parser { */ #match(tokenType: Token['type']): void { const currentToken = this.#currentToken(); - if (currentToken !== tokenType) { + if (currentToken === tokenType) { this.#advance(); } else { const token = this.#lookaheadToken(0); diff --git a/packages/jmespath/src/ast.ts b/packages/jmespath/src/ast.ts index 3b5c53521b..fc20e53fe8 100644 --- a/packages/jmespath/src/ast.ts +++ b/packages/jmespath/src/ast.ts @@ -20,9 +20,8 @@ const currentNode = (): Node => ({ /** * TODO: write docs for expref() - * TODO: finalize types for expref() */ -const expref = (expression: unknown): Node => ({ +const expref = (expression: Node): Node => ({ type: 'expref', children: [expression], }); diff --git a/packages/jmespath/src/constants.ts b/packages/jmespath/src/constants.ts index 5b6ef3dbfe..89ad49e4b1 100644 --- a/packages/jmespath/src/constants.ts +++ b/packages/jmespath/src/constants.ts @@ -40,7 +40,7 @@ const START_IDENTIFIER = new Set(ASCII_LETTERS + '_'); const VALID_IDENTIFIER = new Set(ASCII_LETTERS + DIGITS + '_'); const VALID_NUMBER = new Set(DIGITS); const WHITESPACE = new Set(' \t\n\r'); -const SIMPLE_TOKENS = new Map([ +const SIMPLE_TOKENS: Map = new Map([ ['.', 'dot'], ['*', 'star'], [':', 'colon'], diff --git a/packages/jmespath/tests/unit/unicode.test.ts b/packages/jmespath/tests/unit/unicode.test.ts index b6ad8c63ca..8a03dffe3d 100644 --- a/packages/jmespath/tests/unit/unicode.test.ts +++ b/packages/jmespath/tests/unit/unicode.test.ts @@ -22,11 +22,11 @@ describe('Unicode tests', () => { it.each([ { - expression: '☯', + expression: '"☯"', expected: true, }, { - expression: '☃', + expression: '"☃"', expected: undefined, }, ])( @@ -43,25 +43,9 @@ describe('Unicode tests', () => { } ); - it.each([ - { expression: 'a', expected: undefined }, - { expression: 'b', expected: undefined }, - { expression: 'c', expected: undefined }, - { expression: 'a.b', expected: undefined }, - ])('should parse an array', ({ expression, expected }) => { - // Prepare - const data = ['a', 'b', 'c']; - - // Act - const result = search(expression, data); - - // Assess - expect(result).toStrictEqual(expected); - }); - it.each([ { - expression: '♪♫•*¨*•.¸¸❤¸¸.•*¨*•♫♪', + expression: '"♪♫•*¨*•.¸¸❤¸¸.•*¨*•♫♪"', expected: true, }, ])( From 6eaff375e7c06db2baacd6e1c2bec3a22685f828 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 5 Jun 2023 00:31:54 +0000 Subject: [PATCH 046/103] chore: remove visitor class --- packages/jmespath/src/ParsedResult.ts | 15 +-------------- packages/jmespath/src/visitor/GraphvizVisitor.ts | 14 -------------- 2 files changed, 1 insertion(+), 28 deletions(-) delete mode 100644 packages/jmespath/src/visitor/GraphvizVisitor.ts diff --git a/packages/jmespath/src/ParsedResult.ts b/packages/jmespath/src/ParsedResult.ts index 9e5dd6d97b..6419907cb1 100644 --- a/packages/jmespath/src/ParsedResult.ts +++ b/packages/jmespath/src/ParsedResult.ts @@ -1,4 +1,4 @@ -import { TreeInterpreter, GraphvizVisitor } from './visitor'; +import { TreeInterpreter } from './visitor'; import type { Node } from './types'; class ParsedResult { @@ -10,19 +10,6 @@ class ParsedResult { this.parsed = parsed; } - /** - * Render the parsed AST as a dot file. - * - * TODO: write docs for ParsedResult#renderDotFile() - * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/parser.py#L515-L519 - * - */ - public renderDotFile(): string { - const renderer = new GraphvizVisitor(); - - return renderer.visit(this.parsed); - } - public search(value: unknown, options?: unknown): unknown { const interpreter = new TreeInterpreter(options); diff --git a/packages/jmespath/src/visitor/GraphvizVisitor.ts b/packages/jmespath/src/visitor/GraphvizVisitor.ts deleted file mode 100644 index d9363f43ec..0000000000 --- a/packages/jmespath/src/visitor/GraphvizVisitor.ts +++ /dev/null @@ -1,14 +0,0 @@ -class GraphvizVisitor { - /** - * TODO: write docs for GraphvizVisitor.visit() - * TODO: finalize types for GraphvizVisitor.visit() - * - * @param _node - * @returns - */ - public visit(_node: unknown): string { - return ''; - } -} - -export { GraphvizVisitor }; From 31acfab46cc9dea5adf0a01383884d040003ae36 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 5 Jun 2023 02:45:15 +0000 Subject: [PATCH 047/103] wip: jmespath functions --- packages/jmespath/src/Functions.ts | 187 ++++++++++++++++++ packages/jmespath/src/constants.ts | 4 - .../jmespath/src/visitor/TreeInterpreter.ts | 1 + 3 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 packages/jmespath/src/Functions.ts diff --git a/packages/jmespath/src/Functions.ts b/packages/jmespath/src/Functions.ts new file mode 100644 index 0000000000..6b47894509 --- /dev/null +++ b/packages/jmespath/src/Functions.ts @@ -0,0 +1,187 @@ +import type { JSONArray, JSONValue } from './types'; + +/** + * TODO: validate SignatureDecorator type and extract to a separate file + */ +type SignatureDecorator = ( + target: Functions, + propertyKey: string | symbol, + descriptor: PropertyDescriptor +) => void; + +/** + * TODO: validate SignatureOptions type and extract to a separate file + */ +type SignatureOptions = { + argumentsSpecs: Array>; + variadic?: boolean; +}; + +/** + * TODO: write docs for Functions + */ +class Functions { + // TODO: find a type for FUNCTION_TABLE + public FUNCTION_TABLE: Map = new Map(); + + /** + * Get the absolute value of the provided number. + * + * @param args The number to get the absolute value of + * @returns The absolute value of the number + */ + public funcAbs(args: number): number { + return Math.abs(args); + } + + /** + * Calculate the average of the numbers in the provided array. + * + * @param args The numbers to average + * @returns The average of the numbers + */ + public funcAvg(args: Array): number { + return args.reduce((a, b) => a + b, 0) / args.length; + } + + /** + * Get the first argument that does not evaluate to null. + * If all arguments evaluate to null, then null is returned. + * + * @param args The keys of the items to check + * @returns The first key that is not null or null if all keys are null + */ + public funcNotNull(args: Array): JSONValue | null { + return args.find((arg) => !Object.is(arg, null)) || null; + } + + /** + * Convert the provided value to an array. + * + * If the provided value is an array, then it is returned. + * Otherwise, the value is wrapped in an array and returned. + * + * @param args The items to convert to an array + * @returns The items as an array + */ + public funcToArray( + arg: JSONArray | Array + ): Array | JSONArray { + return Array.isArray(arg) ? arg : [arg]; + } + + /** + * Convert the provided value to a number. + * + * If the provided value is a number, then it is returned. + * Otherwise, the value is converted to a number and returned. + * + * If the value cannot be converted to a number, then null is returned. + * + * @param arg The value to convert to a number + * @returns The value as a number or null if the value cannot be converted to a number + */ + public funcToNumber(arg: JSONValue): number | null { + if (typeof arg === 'number') { + return arg; + } else if (typeof arg === 'string') { + const num = Number(arg); + + return Number.isNaN(num) ? null : num; + } else { + return null; + } + } + + /** + * Convert the provided value to a string. + * + * If the provided value is a string, then it is returned. + * Otherwise, the value is converted to a string and returned. + * + * @param arg The value to convert to a string + * @returns The value as a string + */ + public funcToString(arg: JSONValue): string { + return typeof arg === 'string' ? arg : JSON.stringify(arg); + } + + /** + * TODO: write docs for Functions.signature() + * + * @param options + * @returns + */ + public signature(options: SignatureOptions): SignatureDecorator { + return (_target, _propertyKey, descriptor) => { + const originalMethod = descriptor.value; + if (typeof originalMethod !== 'function') { + throw new TypeError('Only methods can be decorated with @signature.'); + } + const methodName = originalMethod.name; + + // We need to use `functionsRef` instead of `this` because we want to + // access other methods on the class from within the decorated method. + // eslint-disable-next-line @typescript-eslint/no-this-alias + const functionsRef = this; + + // Use a function() {} instead of an () => {} arrow function so that we can + // access `myClass` as `this` in a decorated `myClass.myMethod()`. + descriptor.value = function (args: unknown[]) { + const { variadic, argumentsSpecs } = options; + if (variadic) { + if (args.length < argumentsSpecs.length) { + // TODO: throw VariadictArityError + /* raise exceptions.VariadictArityError( + len(signature), len(args), function_name) */ + } + } else if (args.length !== argumentsSpecs.length) { + // TODO: throw ArityError + /* raise exceptions.ArityError( + len(signature), len(args), function_name) */ + } + + functionsRef.#typeCheck(args, argumentsSpecs, methodName); + + return originalMethod.apply(args); + }; + + return descriptor; + }; + } + + /** + * TODO: write docs for Functions.#typeCheck() + * @param args + * @param argumentsSpecs + * @param name + */ + #typeCheck( + args: unknown[], + argumentsSpecs: Array>, + decoratedFuncName: string + ): void { + argumentsSpecs.forEach((argumentSpec, index) => { + this.#typeCheckArgument(args[index], argumentSpec, decoratedFuncName); + }); + } + + /** + * TODO: write docs for Functions.#typeCheckArgument() + * + * Type checking at runtime involves checking the top level type, + * and in the case of arrays, potentially checking the types of + * the elements in the array. + * + * @param arg + * @param argumentSpec + * @param decoratedFuncName + */ + #typeCheckArgument( + arg: unknown, + argumentSpec: Array, + decoratedFuncName: string + ): void {} +} + +export { Functions }; diff --git a/packages/jmespath/src/constants.ts b/packages/jmespath/src/constants.ts index 89ad49e4b1..0dd6436957 100644 --- a/packages/jmespath/src/constants.ts +++ b/packages/jmespath/src/constants.ts @@ -54,10 +54,6 @@ const SIMPLE_TOKENS: Map = new Map([ ['}', 'rbrace'], ]); -/** - * A map of JavaScript types to JMESPath types. - */ - export { BINDING_POWER, WHITESPACE, diff --git a/packages/jmespath/src/visitor/TreeInterpreter.ts b/packages/jmespath/src/visitor/TreeInterpreter.ts index 221a804137..27def5aead 100644 --- a/packages/jmespath/src/visitor/TreeInterpreter.ts +++ b/packages/jmespath/src/visitor/TreeInterpreter.ts @@ -1,4 +1,5 @@ class TreeInterpreter { + #functions: unknown; /** * TODO: implement TreeInterpreter constructor * @param _options From 5d9154d65d0e86063fa0c397ed64f8ad8c558fcc Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 5 Jun 2023 15:17:07 +0000 Subject: [PATCH 048/103] wip: jmespath functions --- packages/jmespath/src/ParsedResult.ts | 4 +- packages/jmespath/src/Parser.ts | 6 +- packages/jmespath/src/ast.ts | 39 +-- packages/jmespath/src/errors.ts | 34 +- .../jmespath/src/{ => functions}/Functions.ts | 82 ++--- packages/jmespath/src/functions/index.ts | 1 + .../jmespath/src/functions/typeChecking.ts | 110 +++++++ packages/jmespath/src/search.ts | 6 +- packages/jmespath/src/types/AST.ts | 6 +- packages/jmespath/src/types/ParsedResult.ts | 8 + .../jmespath/src/types/TreeInterpreter.ts | 8 + packages/jmespath/src/types/index.ts | 2 + .../jmespath/src/visitor/TreeInterpreter.ts | 306 +++++++++++++++++- packages/jmespath/src/visitor/index.ts | 1 - 14 files changed, 499 insertions(+), 114 deletions(-) rename packages/jmespath/src/{ => functions}/Functions.ts (67%) create mode 100644 packages/jmespath/src/functions/index.ts create mode 100644 packages/jmespath/src/functions/typeChecking.ts create mode 100644 packages/jmespath/src/types/ParsedResult.ts create mode 100644 packages/jmespath/src/types/TreeInterpreter.ts diff --git a/packages/jmespath/src/ParsedResult.ts b/packages/jmespath/src/ParsedResult.ts index 6419907cb1..998544eef8 100644 --- a/packages/jmespath/src/ParsedResult.ts +++ b/packages/jmespath/src/ParsedResult.ts @@ -1,5 +1,5 @@ import { TreeInterpreter } from './visitor'; -import type { Node } from './types'; +import type { Node, JSONValue, ParsingOptions } from './types'; class ParsedResult { public expression: string; @@ -10,7 +10,7 @@ class ParsedResult { this.parsed = parsed; } - public search(value: unknown, options?: unknown): unknown { + public search(value: JSONValue, options?: ParsingOptions): unknown { const interpreter = new TreeInterpreter(options); return interpreter.visit(this.parsed, value); diff --git a/packages/jmespath/src/Parser.ts b/packages/jmespath/src/Parser.ts index 236b393f4f..0308758cae 100644 --- a/packages/jmespath/src/Parser.ts +++ b/packages/jmespath/src/Parser.ts @@ -285,7 +285,7 @@ class Parser { return andExpression(leftNode, right); } else if (tokenType === 'lparen') { - if (leftNode.type !== 'field') { + if (leftNode.type !== 'field' || typeof leftNode.value !== 'string') { // 0 - first func arg or closing parenthesis // -1 - '(' token // -2 - invalid func "name" @@ -437,7 +437,7 @@ class Parser { * @param left * @param right */ - #projectIfSlice(left: Token | Node, right: Token | Node): Node { + #projectIfSlice(left: Node, right: Node): Node { const idxExpression = indexExpression([left, right]); if (right.type === 'slice') { return projection( @@ -456,7 +456,7 @@ class Parser { * @param left * @param comparatorChar */ - #parseComparator(left: Token | Node, comparatorChar: Token['type']): Node { + #parseComparator(left: Node, comparatorChar: Token['type']): Node { return comparator( comparatorChar, left, diff --git a/packages/jmespath/src/ast.ts b/packages/jmespath/src/ast.ts index fc20e53fe8..5769f4238d 100644 --- a/packages/jmespath/src/ast.ts +++ b/packages/jmespath/src/ast.ts @@ -1,9 +1,9 @@ import type { JSONValue, Node } from './types'; + /** * TODO: write docs for comparator() - * TODO: finalize types for comparator() */ -const comparator = (name: unknown, first: unknown, second: unknown): Node => ({ +const comparator = (name: string, first: Node, second: Node): Node => ({ type: 'comparator', children: [first, second], value: name, @@ -11,7 +11,6 @@ const comparator = (name: unknown, first: unknown, second: unknown): Node => ({ /** * TODO: write docs for currentNode() - * TODO: finalize types for currentNode() */ const currentNode = (): Node => ({ type: 'current', @@ -28,9 +27,8 @@ const expref = (expression: Node): Node => ({ /** * TODO: write docs for functionExpression() - * TODO: finalize types for functionExpression() */ -const functionExpression = (name: unknown, args: unknown[]): Node => ({ +const functionExpression = (name: string, args: Node[]): Node => ({ type: 'function_expression', children: args, value: name, @@ -38,9 +36,8 @@ const functionExpression = (name: unknown, args: unknown[]): Node => ({ /** * TODO: write docs for field() - * TODO: finalize types for field() */ -const field = (name: unknown): Node => ({ +const field = (name: JSONValue): Node => ({ type: 'field', children: [], value: name, @@ -48,39 +45,32 @@ const field = (name: unknown): Node => ({ /** * TODO: write docs for fieldExpression() - * TODO: finalize types for fieldExpression() * * @param left * @param right * @param comparator * @returns */ -const filterProjection = ( - left: unknown, - right: unknown, - comparator: unknown -): Node => ({ +const filterProjection = (left: Node, right: Node, comparator: Node): Node => ({ type: 'filter_projection', children: [left, right, comparator], }); /** * TODO: write docs for flatten() - * TODO: finalize types for flatten() * * @param left * @param right * @param comparator * @returns */ -const flatten = (node: unknown): Node => ({ +const flatten = (node: Node): Node => ({ type: 'flatten', children: [node], }); /** * TODO: write docs for identity() - * TODO: finalize types for identity() * * @param left * @param right @@ -91,14 +81,13 @@ const identity = (): Node => ({ type: 'identity', children: [] }); /** * TODO: write docs for index() - * TODO: finalize types for index() * * @param left * @param right * @param comparator * @returns */ -const index = (index: unknown): Node => ({ +const index = (index: JSONValue): Node => ({ type: 'index', value: index, children: [], @@ -106,21 +95,19 @@ const index = (index: unknown): Node => ({ /** * TODO: write docs for indexExpression() - * TODO: finalize types for indexExpression() * * @param left * @param right * @param comparator * @returns */ -const indexExpression = (children: unknown[]): Node => ({ +const indexExpression = (children: Node[]): Node => ({ type: 'index_expression', children: children, }); /** * TODO: write docs for keyValPair() - * TODO: finalize types for keyValPair() * * @param keyName * @param node @@ -133,11 +120,10 @@ const keyValPair = (keyName: JSONValue, node: Node): Node => ({ /** * TODO: write docs for literal() - * TODO: finalize types for literal() * * @param literalValue */ -const literal = (literalValue: unknown): Node => ({ +const literal = (literalValue: JSONValue): Node => ({ type: 'literal', value: literalValue, children: [], @@ -145,7 +131,6 @@ const literal = (literalValue: unknown): Node => ({ /** * TODO: write docs for multiSelectDict() - * TODO: finalize types for multiSelectDict() * TODO: check if multiSelectDict() could be possibly be renamed to multiSelectObject() / multiSelectMap() / multiSelectHash() * * @param nodes @@ -157,7 +142,6 @@ const multiSelectDict = (nodes: Node[]): Node => ({ /** * TODO: write docs for multiSelectList() - * TODO: finalize types for multiSelectList() * TODO: check if multiSelectList() could be possibly be renamed to multiSelectArray() * * @param nodes @@ -228,14 +212,15 @@ const subexpression = (children: Node[]): Node => ({ /** * TODO: write docs for slice() + * TODO: fix type for slice() * * @param start * @param end * @param step */ -const slice = (start: unknown, end: unknown, step: unknown): Node => ({ +const slice = (start: JSONValue, end: JSONValue, step: JSONValue): Node => ({ type: 'slice', - children: [start, end, step], + children: [start as Node, end as Node, step as Node], }); /** diff --git a/packages/jmespath/src/errors.ts b/packages/jmespath/src/errors.ts index 728e8702e6..460a846361 100644 --- a/packages/jmespath/src/errors.ts +++ b/packages/jmespath/src/errors.ts @@ -38,8 +38,6 @@ class LexerError extends JMESPathError { /** * TODO: write docs for ParseError - * - * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/exceptions.py#L9 */ class ParseError extends JMESPathError { /** @@ -93,10 +91,8 @@ class IncompleteExpressionError extends ParseError { /** * TODO: write docs for ArityError - * TODO: complete ArityError implementation - * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/exceptions.py#LL66C1-L85C30 */ -/* class ArityError extends ParseError { +class ArityError extends JMESPathError { public actualArity: number; public expectedArity: number; public functionName: string; @@ -106,25 +102,37 @@ class IncompleteExpressionError extends ParseError { actualArity: number; functionName: string; }) { + super('Invalid arity for JMESPath function'); this.name = 'ArityError'; this.actualArity = options.actualArity; this.expectedArity = options.expectedArity; this.functionName = options.functionName; + + // Set the message to include the error info. + this.message = `Expected at least ${this.expectedArity} ${this.pluralize( + 'argument', + this.expectedArity + )} for function ${this.functionName}, received: ${this.actualArity}`; } - #pluralize(word: string, count: number): string { + protected pluralize(word: string, count: number): string { return count === 1 ? word : `${word}s`; } -} */ +} /** * TODO: write docs for VariadicArityError - * TODO: complete VariadicArityError implementation - * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/exceptions.py#L89-L96 - * TODO: change extends to ArityError - * TODO: add `name` to `VariadicArityError` */ -class VariadicArityError extends ParseError {} +class VariadicArityError extends ArityError { + public constructor(options: { + expectedArity: number; + actualArity: number; + functionName: string; + }) { + super(options); + this.name = 'VariadicArityError'; + } +} /** * TODO: write docs for JMESPathTypeError @@ -175,7 +183,7 @@ export { LexerError, ParseError, IncompleteExpressionError, - // ArityError, + ArityError, VariadicArityError, JMESPathTypeError, EmptyExpressionError, diff --git a/packages/jmespath/src/Functions.ts b/packages/jmespath/src/functions/Functions.ts similarity index 67% rename from packages/jmespath/src/Functions.ts rename to packages/jmespath/src/functions/Functions.ts index 6b47894509..6cc1027873 100644 --- a/packages/jmespath/src/Functions.ts +++ b/packages/jmespath/src/functions/Functions.ts @@ -1,4 +1,5 @@ -import type { JSONArray, JSONValue } from './types'; +import type { JSONArray, JSONValue } from '../types'; +import { typeCheck, arityCheck } from './typeChecking'; /** * TODO: validate SignatureDecorator type and extract to a separate file @@ -30,6 +31,7 @@ class Functions { * @param args The number to get the absolute value of * @returns The absolute value of the number */ + @Functions.signature({ argumentsSpecs: [['number']] }) public funcAbs(args: number): number { return Math.abs(args); } @@ -40,10 +42,25 @@ class Functions { * @param args The numbers to average * @returns The average of the numbers */ + @Functions.signature({ + argumentsSpecs: [['array-number']], + }) public funcAvg(args: Array): number { return args.reduce((a, b) => a + b, 0) / args.length; } + /** + * Determine if the provided value is contained in the provided item. + * TODO: write docs for funcContains() + */ + @Functions.signature({ + argumentsSpecs: [['array', 'string'], ['any']], + }) + public funcContains(haystack: string, needle: string): boolean { + // TODO: review this implementation + return haystack.includes(needle); + } + /** * Get the first argument that does not evaluate to null. * If all arguments evaluate to null, then null is returned. @@ -51,6 +68,10 @@ class Functions { * @param args The keys of the items to check * @returns The first key that is not null or null if all keys are null */ + @Functions.signature({ + argumentsSpecs: [[]], + variadic: true, + }) public funcNotNull(args: Array): JSONValue | null { return args.find((arg) => !Object.is(arg, null)) || null; } @@ -112,7 +133,7 @@ class Functions { * @param options * @returns */ - public signature(options: SignatureOptions): SignatureDecorator { + public static signature(options: SignatureOptions): SignatureDecorator { return (_target, _propertyKey, descriptor) => { const originalMethod = descriptor.value; if (typeof originalMethod !== 'function') { @@ -120,68 +141,19 @@ class Functions { } const methodName = originalMethod.name; - // We need to use `functionsRef` instead of `this` because we want to - // access other methods on the class from within the decorated method. - // eslint-disable-next-line @typescript-eslint/no-this-alias - const functionsRef = this; - // Use a function() {} instead of an () => {} arrow function so that we can // access `myClass` as `this` in a decorated `myClass.myMethod()`. descriptor.value = function (args: unknown[]) { const { variadic, argumentsSpecs } = options; - if (variadic) { - if (args.length < argumentsSpecs.length) { - // TODO: throw VariadictArityError - /* raise exceptions.VariadictArityError( - len(signature), len(args), function_name) */ - } - } else if (args.length !== argumentsSpecs.length) { - // TODO: throw ArityError - /* raise exceptions.ArityError( - len(signature), len(args), function_name) */ - } - - functionsRef.#typeCheck(args, argumentsSpecs, methodName); - - return originalMethod.apply(args); + arityCheck(args, argumentsSpecs, methodName, variadic); + typeCheck(args, argumentsSpecs, methodName); + + return originalMethod.apply(this, args); }; return descriptor; }; } - - /** - * TODO: write docs for Functions.#typeCheck() - * @param args - * @param argumentsSpecs - * @param name - */ - #typeCheck( - args: unknown[], - argumentsSpecs: Array>, - decoratedFuncName: string - ): void { - argumentsSpecs.forEach((argumentSpec, index) => { - this.#typeCheckArgument(args[index], argumentSpec, decoratedFuncName); - }); - } - - /** - * TODO: write docs for Functions.#typeCheckArgument() - * - * Type checking at runtime involves checking the top level type, - * and in the case of arrays, potentially checking the types of - * the elements in the array. - * - * @param arg - * @param argumentSpec - * @param decoratedFuncName - */ - #typeCheckArgument( - arg: unknown, - argumentSpec: Array, - decoratedFuncName: string - ): void {} } export { Functions }; diff --git a/packages/jmespath/src/functions/index.ts b/packages/jmespath/src/functions/index.ts new file mode 100644 index 0000000000..049f75632b --- /dev/null +++ b/packages/jmespath/src/functions/index.ts @@ -0,0 +1 @@ +export * from './Functions'; diff --git a/packages/jmespath/src/functions/typeChecking.ts b/packages/jmespath/src/functions/typeChecking.ts new file mode 100644 index 0000000000..2fd84eaac2 --- /dev/null +++ b/packages/jmespath/src/functions/typeChecking.ts @@ -0,0 +1,110 @@ +import { JMESPathTypeError, ArityError, VariadicArityError } from '../errors'; + +/** + * TODO: write docs for arityCheck() + * + * @param args + * @param argumentsSpecs + * @param decoratedFuncName + * @param variadic + */ +const arityCheck = ( + args: unknown[], + argumentsSpecs: Array>, + decoratedFuncName: string, + variadic?: boolean +): void => { + if (variadic) { + if (args.length < argumentsSpecs.length) { + throw new VariadicArityError({ + functionName: decoratedFuncName, + expectedArity: argumentsSpecs.length, + actualArity: args.length, + }); + } + } else if (args.length !== argumentsSpecs.length) { + throw new ArityError({ + functionName: decoratedFuncName, + expectedArity: argumentsSpecs.length, + actualArity: args.length, + }); + } +}; + +/** + * TODO: write docs for typeCheck() + * @param args + * @param argumentsSpecs + * @param name + */ +const typeCheck = ( + args: unknown[], + argumentsSpecs: Array>, + decoratedFuncName: string +): void => { + argumentsSpecs.forEach((argumentSpec, index) => { + typeCheckArgument(args[index], argumentSpec, decoratedFuncName); + }); +}; + +/** + * TODO: write docs for Functions.#typeCheckArgument() + * + * Type checking at runtime involves checking the top level type, + * and in the case of arrays, potentially checking the types of + * the elements in the array. + * + * @param arg + * @param argumentSpec + * @param decoratedFuncName + */ +const typeCheckArgument = ( + arg: unknown, + argumentSpec: Array, + decoratedFuncName: string +): void => { + // TODO: check if all types in argumentSpec are valid + if (argumentSpec.length === 0 || argumentSpec[0] === 'any') { + return; + } + argumentSpec.forEach((type) => { + if (type.startsWith('array')) { + if (!Array.isArray(arg)) { + throw new JMESPathTypeError({ + functionName: decoratedFuncName, + currentValue: arg, + expectedTypes: argumentSpec.join(', '), + actualType: typeof arg, + }); + } + if (type.includes('-')) { + const arrayItemsType = type.slice(6); + arg.forEach((element) => { + typeCheckArgument(element, [arrayItemsType], decoratedFuncName); + }); + } + } else { + if (type === 'string' || type === 'number' || type === 'boolean') { + if (typeof arg !== type) { + throw new JMESPathTypeError({ + functionName: decoratedFuncName, + currentValue: arg, + expectedTypes: argumentSpec.join(', '), + actualType: typeof arg, + }); + } + } else if (type === 'null') { + if (!Object.is(arg, null)) { + throw new JMESPathTypeError({ + functionName: decoratedFuncName, + currentValue: arg, + expectedTypes: argumentSpec.join(', '), + actualType: typeof arg, + }); + } + } + } + }); +}; + +export { arityCheck, typeCheck }; diff --git a/packages/jmespath/src/search.ts b/packages/jmespath/src/search.ts index cdaee25dec..7492a8b901 100644 --- a/packages/jmespath/src/search.ts +++ b/packages/jmespath/src/search.ts @@ -1,8 +1,8 @@ import { Parser } from './Parser'; +import type { JSONValue, ParsingOptions } from './types'; /** * TODO: write docs for search() - * TODO: fix types for search() * * @param expression * @param data @@ -11,8 +11,8 @@ import { Parser } from './Parser'; */ const search = ( expression: string, - data: unknown, - options?: unknown + data: JSONValue, + options?: ParsingOptions ): unknown => { return new Parser().parse(expression).search(data, options); }; diff --git a/packages/jmespath/src/types/AST.ts b/packages/jmespath/src/types/AST.ts index bdfcc4d23f..b54c449fcc 100644 --- a/packages/jmespath/src/types/AST.ts +++ b/packages/jmespath/src/types/AST.ts @@ -1,10 +1,12 @@ +import { JSONValue } from './JSON'; + /** * TODO: write docs for Node type */ type Node = { type: string; - children: unknown[]; - value?: unknown; + children: Node[]; + value?: JSONValue; }; export { Node }; diff --git a/packages/jmespath/src/types/ParsedResult.ts b/packages/jmespath/src/types/ParsedResult.ts new file mode 100644 index 0000000000..f7ff92c435 --- /dev/null +++ b/packages/jmespath/src/types/ParsedResult.ts @@ -0,0 +1,8 @@ +import { TreeInterpreterOptions } from './TreeInterpreter'; + +/** + * TODO: write docs for ParsingOptions type + */ +type ParsingOptions = TreeInterpreterOptions; + +export type { ParsingOptions }; diff --git a/packages/jmespath/src/types/TreeInterpreter.ts b/packages/jmespath/src/types/TreeInterpreter.ts new file mode 100644 index 0000000000..aaa9fc6583 --- /dev/null +++ b/packages/jmespath/src/types/TreeInterpreter.ts @@ -0,0 +1,8 @@ +import type { Functions } from '../functions'; + +/** + * TODO: write docs for TreeInterpreterOptions type + */ +type TreeInterpreterOptions = { customFunctions?: Functions }; + +export { TreeInterpreterOptions }; diff --git a/packages/jmespath/src/types/index.ts b/packages/jmespath/src/types/index.ts index 43ce21113c..dc9bb68082 100644 --- a/packages/jmespath/src/types/index.ts +++ b/packages/jmespath/src/types/index.ts @@ -1,3 +1,5 @@ export * from './Token'; export * from './JSON'; export * from './AST'; +export * from './TreeInterpreter'; +export * from './ParsedResult'; diff --git a/packages/jmespath/src/visitor/TreeInterpreter.ts b/packages/jmespath/src/visitor/TreeInterpreter.ts index 27def5aead..a886be92f0 100644 --- a/packages/jmespath/src/visitor/TreeInterpreter.ts +++ b/packages/jmespath/src/visitor/TreeInterpreter.ts @@ -1,23 +1,313 @@ +import type { JSONValue, Node, TreeInterpreterOptions } from '../types'; +import { Functions } from '../functions'; + +/** + * TODO: write docs for isRecord() type guard + * + * @param value + * @returns + */ +const isRecord = (value: unknown): value is Record => { + return ( + Object.prototype.toString.call(value) === '[object Object]' && + !Object.is(value, null) + ); +}; + class TreeInterpreter { - #functions: unknown; + #functions: Functions; + /** - * TODO: implement TreeInterpreter constructor * @param _options */ - public constructor(_options?: unknown) { - // Do nothing. + public constructor(options?: TreeInterpreterOptions) { + if (options?.customFunctions) { + this.#functions = options.customFunctions; + } else { + this.#functions = new Functions(); + } } + /** * TODO: write docs for TreeInterpreter.visit() * TODO: finalize types for TreeInterpreter.visit() * - * @param _node - * @param _value + * @param node + * @param value + * @returns + */ + public visit(node: Node, value: JSONValue): JSONValue { + const nodeType = node.type; + if (nodeType === 'subexpression') { + return this.#visitSubexpression(node, value); + } else if (nodeType === 'field') { + return this.#visitField(node, value); + /* } else if (nodeType === 'comparator') { + return this.#visitComparator(node, value); + } else if (nodeType === 'current') { + return this.#visitCurrent(node, value); + } else if (nodeType === 'expref') { + return this.#visitExpref(node, value); + } else if (nodeType === 'filter_projection') { + return this.#visitFilterProjection(node, value); */ + } else if (nodeType === 'function_expression') { + return this.#visitFunctionExpression(node, value); + } else if (nodeType === 'current') { + return this.#visitCurrent(node, value); + } else if (nodeType === 'identity') { + return this.#visitIdentity(node, value); + } else { + // TODO: convert to a custom error + throw new Error(`Not Implemented: Invalid node type: ${node.type}`); + } + } + + /** + * TODO: write docs for TreeInterpreter.visitSubexpression() + * @param node + * @param value + * @returns + */ + #visitSubexpression(node: Node, value: JSONValue): JSONValue { + let result = value; + for (const child of node.children) { + result = this.visit(child, result); + } + + return result; + } + + /** + * TODO: write docs for TreeInterpreter.visitField() + * @param node + * @param value + * @returns + */ + #visitField(node: Node, value: JSONValue): JSONValue { + if (!node.value) return null; + if ( + isRecord(value) && + typeof node.value === 'string' && + node.value in value + ) { + return value[node.value]; + } else { + return null; + } + } + + /* #visitComparator(node: Node, value: JSONValue): JSONValue { + return true; + } + */ + + /** + * TODO: write docs for TreeInterpreter.visitCurrent() + * @param node + * @param value + * @returns + */ + #visitCurrent(_node: Node, value: JSONValue): JSONValue { + return value; + } + /* + + #visitExpref(node: Node, value: JSONValue): JSONValue { + return true; + } + */ + + /** + * TODO: write docs for TreeInterpreter.visitFunctionExpression() + * @param node + * @param value + * @returns + */ + #visitFunctionExpression(node: Node, value: JSONValue): JSONValue { + const args = []; + for (const child of node.children) { + args.push(this.visit(child, value)); + } + const methods = Object.getOwnPropertyNames( + Object.getPrototypeOf(this.#functions) + ); + const methodName = methods.find( + (method) => method.replace('func', '').toLowerCase() === node.value + ); + if (!methodName) { + // TODO: convert to a custom error + throw new Error(`Function not found: ${methodName}`); + } + + // We know that methodName is a key of this.#functions, but TypeScript + // doesn't know that, so we have to use @ts-ignore to tell it that it's + // okay. We could use a type assertion like `as keyof Functions`, but + // we also want to keep the args generic, so for now we'll just ignore it. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-next-line + return this.#functions[methodName](args); + } + + /** + * TODO: write docs for TreeInterpreter.visitIndex() + * @param node + * @param value + * @returns + */ + /* #visitFilterProjection(node: Node, value: JSONValue): JSONValue { + return true; + } */ + + /** + * TODO: write docs for TreeInterpreter.visitIndex() + * @param node + * @param value + * @returns + */ + /* #visitFlatten(node: Node, value: JSONValue): JSONValue { + return true; + } */ + + /** + * TODO: write docs for TreeInterpreter.visitIdentity() + * @param node + * @param value * @returns */ - public visit(_node: unknown, _value: unknown): unknown { - return ''; + #visitIdentity(_node: Node, value: JSONValue): JSONValue { + return value; } + + /** + * TODO: write docs for TreeInterpreter.visitIndex() + * @param node + * @param value + * @returns + */ + /* #visitIndex(node: Node, value: JSONValue): JSONValue { + return true; + } */ + + /** + * TODO: write docs for TreeInterpreter.visitIndexExpression() + * @param node + * @param value + * @returns + */ + /* #visitIndexExpression(node: Node, value: JSONValue): JSONValue { + return true; + } */ + + /** + * TODO: write docs for TreeInterpreter.visitSlice() + * @param node + * @param value + * @returns + */ + /* #visitSlice(node: Node, value: JSONValue): JSONValue { + + return true; + } */ + + /** + * TODO: write docs for TreeInterpreter.visitKeyValPair() + * @param node + * @param value + * @returns + */ + /* #visitKeyValPair(node: Node, value: JSONValue): JSONValue { + return true; + } */ + + /** + * TODO: write docs for TreeInterpreter.visitLiteral() + * @param node + * @param value + * @returns + */ + /* #visitLiteral(node: Node, value: JSONValue): JSONValue { + return true; + } */ + + /** + * TODO: write docs for TreeInterpreter.visitMultiSelectDict() + * @param node + * @param value + * @returns + */ + /* #visitMultiSelectDict(node: Node, value: JSONValue): JSONValue { + return true; + } */ + + /** + * TODO: write docs for TreeInterpreter.visitMultiSelectList() + * @param node + * @param value + * @returns + */ + /* #visitMultiSelectList(node: Node, value: JSONValue): JSONValue { + return true; + } */ + + /** + * TODO: write docs for TreeInterpreter.visitOrExpression() + * @param node + * @param value + * @returns + */ + /* #visitOrExpression(node: Node, value: JSONValue): JSONValue { + return true; + } */ + + /** + * TODO: write docs for TreeInterpreter.visitAndExpression() + * @param node + * @param value + * @returns + */ + /* #visitAndExpression(node: Node, value: JSONValue): JSONValue { + return true; + } */ + + /** + * TODO: write docs for TreeInterpreter.visitNotExpression() + * @param node + * @param value + * @returns + */ + /* #visitNotExpression(node: Node, value: JSONValue): JSONValue { + return true; + } */ + + /** + * TODO: write docs for TreeInterpreter.visitPipe() + * @param node + * @param value + * @returns + */ + /* #visitPipe(node: Node, value: JSONValue): JSONValue { + return true; + } */ + + /** + * TODO: write docs for TreeInterpreter.visitProjection() + * @param node + * @param value + * @returns + */ + /* #visitProjection(node: Node, value: JSONValue): JSONValue { + return true; + } */ + + /** + * TODO: write docs for TreeInterpreter.visitValueProjection() + * @param node + * @param value + * @returns + */ + /* #visitValueProjection(node: Node, value: JSONValue): JSONValue { + const base = this.visit(node.children[0], value); + } */ } export { TreeInterpreter }; diff --git a/packages/jmespath/src/visitor/index.ts b/packages/jmespath/src/visitor/index.ts index 8a70f8f104..943851fc79 100644 --- a/packages/jmespath/src/visitor/index.ts +++ b/packages/jmespath/src/visitor/index.ts @@ -1,2 +1 @@ export * from './TreeInterpreter'; -export * from './GraphvizVisitor'; From da42d543dfac83e3681febe1ae71ddd9dc6bb0d8 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 5 Jun 2023 22:06:08 +0000 Subject: [PATCH 049/103] wip: jmespath treeinterpreter --- packages/jmespath/src/functions/Functions.ts | 76 ++++++ .../jmespath/src/visitor/TreeInterpreter.ts | 238 +++++++++++++----- packages/jmespath/src/visitor/utils.ts | 50 ++++ 3 files changed, 304 insertions(+), 60 deletions(-) create mode 100644 packages/jmespath/src/visitor/utils.ts diff --git a/packages/jmespath/src/functions/Functions.ts b/packages/jmespath/src/functions/Functions.ts index 6cc1027873..3de932e6a4 100644 --- a/packages/jmespath/src/functions/Functions.ts +++ b/packages/jmespath/src/functions/Functions.ts @@ -49,6 +49,17 @@ class Functions { return args.reduce((a, b) => a + b, 0) / args.length; } + /** + * Get the ceiling of the provided number. + * + * @param args The number to get the ceiling of + * @returns The ceiling of the number + */ + @Functions.signature({ argumentsSpecs: [['number']] }) + public funcCeil(args: number): number { + return Math.ceil(args); + } + /** * Determine if the provided value is contained in the provided item. * TODO: write docs for funcContains() @@ -61,6 +72,43 @@ class Functions { return haystack.includes(needle); } + /** + * Determines if the provided string ends with the provided suffix. + * + * @param args The string to check + * @returns True if the string ends with the suffix, false otherwise + */ + @Functions.signature({ + argumentsSpecs: [['string'], ['string']], + }) + public funcEndsWith(str: string, suffix: string): boolean { + return str.endsWith(suffix); + } + + /** + * Get the floor of the provided number. + * + * @param args The number to get the floor of + * @returns The floor of the number + */ + @Functions.signature({ argumentsSpecs: [['number']] }) + public funcFloor(args: number): number { + return Math.floor(args); + } + + /** + * Get the number of items in the provided item. + * + * @param args The array to get the length of + * @returns The length of the array + */ + @Functions.signature({ + argumentsSpecs: [['array', 'string']], + }) + public funcLength(arg: string | Array): number { + return arg.length; + } + /** * Get the first argument that does not evaluate to null. * If all arguments evaluate to null, then null is returned. @@ -76,6 +124,34 @@ class Functions { return args.find((arg) => !Object.is(arg, null)) || null; } + /** + * Reverses the provided string or array. + * + * @param args The string or array to reverse + * @returns The reversed string or array + */ + @Functions.signature({ + argumentsSpecs: [['string', 'array']], + }) + public funcReverse(arg: string | Array): string | Array { + return Array.isArray(arg) + ? arg.reverse() + : arg.split('').reverse().join(''); + } + + /** + * Determines if the provided string starts with the provided suffix. + * + * @param args The string to check + * @returns True if the string ends with the suffix, false otherwise + */ + @Functions.signature({ + argumentsSpecs: [['string'], ['string']], + }) + public funcStartsWith(str: string, suffix: string): boolean { + return str.startsWith(suffix); + } + /** * Convert the provided value to an array. * diff --git a/packages/jmespath/src/visitor/TreeInterpreter.ts b/packages/jmespath/src/visitor/TreeInterpreter.ts index a886be92f0..6c6b091dd1 100644 --- a/packages/jmespath/src/visitor/TreeInterpreter.ts +++ b/packages/jmespath/src/visitor/TreeInterpreter.ts @@ -1,18 +1,6 @@ import type { JSONValue, Node, TreeInterpreterOptions } from '../types'; import { Functions } from '../functions'; - -/** - * TODO: write docs for isRecord() type guard - * - * @param value - * @returns - */ -const isRecord = (value: unknown): value is Record => { - return ( - Object.prototype.toString.call(value) === '[object Object]' && - !Object.is(value, null) - ); -}; +import { Expression, isRecord, isTruthy } from './utils'; class TreeInterpreter { #functions: Functions; @@ -43,19 +31,48 @@ class TreeInterpreter { } else if (nodeType === 'field') { return this.#visitField(node, value); /* } else if (nodeType === 'comparator') { - return this.#visitComparator(node, value); + return this.#visitComparator(node, value); */ } else if (nodeType === 'current') { return this.#visitCurrent(node, value); } else if (nodeType === 'expref') { + // TODO: review #visitExpref() return type + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-next-line return this.#visitExpref(node, value); - } else if (nodeType === 'filter_projection') { - return this.#visitFilterProjection(node, value); */ } else if (nodeType === 'function_expression') { return this.#visitFunctionExpression(node, value); - } else if (nodeType === 'current') { - return this.#visitCurrent(node, value); + } else if (nodeType === 'filter_projection') { + return this.#visitFilterProjection(node, value); + } else if (nodeType === 'flatten') { + return this.#visitFlatten(node, value); } else if (nodeType === 'identity') { return this.#visitIdentity(node, value); + } else if (nodeType === 'index') { + return this.#visitIndex(node, value); + } else if (nodeType === 'index_expression') { + return this.#visitIndexExpression(node, value); + /* } else if (nodeType === 'slice') { + return this.#visitSlice(node, value); */ + } else if (nodeType === 'key_val_pair') { + return this.#visitKeyValPair(node, value); + } else if (nodeType === 'literal') { + return this.#visitLiteral(node, value); + } else if (nodeType === 'multi_select_dict') { + return this.#visitMultiSelectDict(node, value); + } else if (nodeType === 'multi_select_list') { + return this.#visitMultiSelectList(node, value); + } else if (nodeType === 'or_expression') { + return this.#visitOrExpression(node, value); + } else if (nodeType === 'and_expression') { + return this.#visitAndExpression(node, value); + /* } else if (nodeType === 'not_expression') { + return this.#visitNotExpression(node, value); */ + } else if (nodeType === 'pipe') { + return this.#visitPipe(node, value); + } else if (nodeType === 'projection') { + return this.#visitProjection(node, value); + /* } else if (nodeType === 'value_projection') { + return this.#visitValueProjection(node, value); */ } else { // TODO: convert to a custom error throw new Error(`Not Implemented: Invalid node type: ${node.type}`); @@ -96,6 +113,12 @@ class TreeInterpreter { } } + /** + * TODO: write docs for TreeInterpreter.visitComparator() + * @param node + * @param value + * @returns + */ /* #visitComparator(node: Node, value: JSONValue): JSONValue { return true; } @@ -110,12 +133,16 @@ class TreeInterpreter { #visitCurrent(_node: Node, value: JSONValue): JSONValue { return value; } - /* - #visitExpref(node: Node, value: JSONValue): JSONValue { - return true; + /** + * TODO: write docs for TreeInterpreter.visitExpref() + * @param node + * @param value + * @returns + */ + #visitExpref(node: Node, _value: JSONValue): Expression { + return new Expression(node.children[0], this); } - */ /** * TODO: write docs for TreeInterpreter.visitFunctionExpression() @@ -149,24 +176,52 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visitIndex() + * TODO: write docs for TreeInterpreter.visitFilterProjection() * @param node * @param value * @returns */ - /* #visitFilterProjection(node: Node, value: JSONValue): JSONValue { - return true; - } */ + #visitFilterProjection(node: Node, value: JSONValue): JSONValue { + const base = this.visit(node.children[0], value); + if (!Array.isArray(base)) { + return null; + } + const comparatorNode = node.children[2]; + const collected = []; + for (const item of base) { + if (isTruthy(this.visit(comparatorNode, item))) { + const current = this.visit(node.children[1], item); + if (current !== null) { + collected.push(current); + } + } + } + + return collected; + } /** - * TODO: write docs for TreeInterpreter.visitIndex() + * TODO: write docs for TreeInterpreter.visitFlatten() * @param node * @param value * @returns */ - /* #visitFlatten(node: Node, value: JSONValue): JSONValue { - return true; - } */ + #visitFlatten(node: Node, value: JSONValue): JSONValue { + const base = this.visit(node.children[0], value); + if (!Array.isArray(base)) { + return null; + } + const mergedList = []; + for (const item of base) { + if (Array.isArray(item)) { + mergedList.push(...item); + } else { + mergedList.push(item); + } + } + + return mergedList; + } /** * TODO: write docs for TreeInterpreter.visitIdentity() @@ -184,9 +239,22 @@ class TreeInterpreter { * @param value * @returns */ - /* #visitIndex(node: Node, value: JSONValue): JSONValue { - return true; - } */ + #visitIndex(node: Node, value: JSONValue): JSONValue { + // The Python implementation doesn't support string indexing + // even though we could, so we won't either for now. + if (!Array.isArray(value)) { + return null; + } + if (typeof node.value !== 'number') { + throw new Error(`Invalid index: ${node.value}`); + } + const found = value[node.value]; + if (found === undefined) { + return null; + } + + return found; + } /** * TODO: write docs for TreeInterpreter.visitIndexExpression() @@ -194,9 +262,14 @@ class TreeInterpreter { * @param value * @returns */ - /* #visitIndexExpression(node: Node, value: JSONValue): JSONValue { - return true; - } */ + #visitIndexExpression(node: Node, value: JSONValue): JSONValue { + let result = value; + for (const child of node.children) { + result = this.visit(child, result); + } + + return result; + } /** * TODO: write docs for TreeInterpreter.visitSlice() @@ -215,9 +288,9 @@ class TreeInterpreter { * @param value * @returns */ - /* #visitKeyValPair(node: Node, value: JSONValue): JSONValue { - return true; - } */ + #visitKeyValPair(node: Node, value: JSONValue): JSONValue { + return this.visit(node.children[0], value); + } /** * TODO: write docs for TreeInterpreter.visitLiteral() @@ -225,9 +298,9 @@ class TreeInterpreter { * @param value * @returns */ - /* #visitLiteral(node: Node, value: JSONValue): JSONValue { - return true; - } */ + #visitLiteral(node: Node, _value: JSONValue): JSONValue { + return node.value || null; + } /** * TODO: write docs for TreeInterpreter.visitMultiSelectDict() @@ -235,9 +308,19 @@ class TreeInterpreter { * @param value * @returns */ - /* #visitMultiSelectDict(node: Node, value: JSONValue): JSONValue { - return true; - } */ + #visitMultiSelectDict(node: Node, value: JSONValue): JSONValue { + if (Object.is(value, null)) { + return null; + } + const collected: JSONValue = {}; + for (const child of node.children) { + if (typeof child.value === 'string') { + collected[child.value] = this.visit(child, value); + } + } + + return collected; + } /** * TODO: write docs for TreeInterpreter.visitMultiSelectList() @@ -245,9 +328,17 @@ class TreeInterpreter { * @param value * @returns */ - /* #visitMultiSelectList(node: Node, value: JSONValue): JSONValue { - return true; - } */ + #visitMultiSelectList(node: Node, value: JSONValue): JSONValue { + if (Object.is(value, null)) { + return null; + } + const collected = []; + for (const child of node.children) { + collected.push(this.visit(child, value)); + } + + return collected; + } /** * TODO: write docs for TreeInterpreter.visitOrExpression() @@ -255,9 +346,14 @@ class TreeInterpreter { * @param value * @returns */ - /* #visitOrExpression(node: Node, value: JSONValue): JSONValue { - return true; - } */ + #visitOrExpression(node: Node, value: JSONValue): JSONValue { + const matched = this.visit(node.children[0], value); + if (!isTruthy(matched)) { + return matched; + } + + return this.visit(node.children[1], value); + } /** * TODO: write docs for TreeInterpreter.visitAndExpression() @@ -265,9 +361,14 @@ class TreeInterpreter { * @param value * @returns */ - /* #visitAndExpression(node: Node, value: JSONValue): JSONValue { - return true; - } */ + #visitAndExpression(node: Node, value: JSONValue): JSONValue { + const matched = this.visit(node.children[0], value); + if (!isTruthy(matched)) { + return matched; + } + + return this.visit(node.children[1], value); + } /** * TODO: write docs for TreeInterpreter.visitNotExpression() @@ -285,9 +386,14 @@ class TreeInterpreter { * @param value * @returns */ - /* #visitPipe(node: Node, value: JSONValue): JSONValue { - return true; - } */ + #visitPipe(node: Node, value: JSONValue): JSONValue { + let result = value; + for (const child of node.children) { + result = this.visit(child, result); + } + + return result; + } /** * TODO: write docs for TreeInterpreter.visitProjection() @@ -295,9 +401,21 @@ class TreeInterpreter { * @param value * @returns */ - /* #visitProjection(node: Node, value: JSONValue): JSONValue { - return true; - } */ + #visitProjection(node: Node, value: JSONValue): JSONValue { + const base = this.visit(node.children[0], value); + if (!Array.isArray(base)) { + return null; + } + const collected = []; + for (const item of base) { + const current = this.visit(node.children[1], item); + if (current !== null) { + collected.push(current); + } + } + + return collected; + } /** * TODO: write docs for TreeInterpreter.visitValueProjection() diff --git a/packages/jmespath/src/visitor/utils.ts b/packages/jmespath/src/visitor/utils.ts new file mode 100644 index 0000000000..88a54684b0 --- /dev/null +++ b/packages/jmespath/src/visitor/utils.ts @@ -0,0 +1,50 @@ +import type { TreeInterpreter } from './TreeInterpreter'; +import type { Node, JSONValue } from '../types'; + +/** + * TODO: write docs for Expression + * TODO: see if #expression is needed + */ +class Expression { + readonly #expression: Node; + readonly #interpreter: TreeInterpreter; + + public constructor(expression: Node, interpreter: TreeInterpreter) { + this.#expression = expression; + this.#interpreter = interpreter; + } + + public visit(node: Node, value: JSONValue): JSONValue { + return this.#interpreter.visit(node, value); + } +} + +/** + * TODO: write docs for isRecord() type guard + * + * @param value + * @returns + */ +const isRecord = (value: unknown): value is Record => { + return ( + Object.prototype.toString.call(value) === '[object Object]' && + !Object.is(value, null) + ); +}; + +/** + * TODO: write docs for isTruthy() + * @param value + * @returns + */ +const isTruthy = (value: unknown): boolean => { + return ( + value !== '' || + (Array.isArray(value) && value.length > 0) || + (isRecord(value) && Object(value).keys().length > 0) || + value !== undefined || + Object.is(value, true) + ); +}; + +export { Expression, isRecord, isTruthy }; From 30c9eca16930fa09a8fc7460e9180097b987501e Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 5 Jun 2023 22:08:51 +0000 Subject: [PATCH 050/103] chore: exports --- packages/jmespath/package.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/jmespath/package.json b/packages/jmespath/package.json index 58e45b44e6..53c2a3c479 100644 --- a/packages/jmespath/package.json +++ b/packages/jmespath/package.json @@ -24,10 +24,16 @@ "*.ts": "npm run lint-fix", "*.js": "npm run lint-fix" }, - "homepage": "https://github.com/awslabs/aws-lambda-powertools-typescript/tree/main/packages/jmespath#readme", - "license": "MIT-0", + "exports": { + ".": { + "import": "./lib/index.js", + "require": "./lib/index.js" + } + }, "main": "./lib/index.js", "types": "./lib/index.d.ts", + "homepage": "https://github.com/awslabs/aws-lambda-powertools-typescript/tree/main/packages/jmespath#readme", + "license": "MIT-0", "files": [ "lib" ], From f53ac171bd4dbcaafd3fc0e1f4f7692b77b9dfed Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 6 Jun 2023 15:29:06 +0000 Subject: [PATCH 051/103] tests: added group header --- packages/jmespath/tests/unit/base.test.ts | 5 + packages/jmespath/tests/unit/boolean.test.ts | 5 + packages/jmespath/tests/unit/current.test.ts | 5 + packages/jmespath/tests/unit/escape.test.ts | 5 + packages/jmespath/tests/unit/filters.test.ts | 902 +++++++++++++++++- .../jmespath/tests/unit/functions.test.ts | 5 + .../jmespath/tests/unit/identifiers.test.ts | 5 + packages/jmespath/tests/unit/indices.test.ts | 5 + packages/jmespath/tests/unit/literal.test.ts | 5 + .../jmespath/tests/unit/multiselect.test.ts | 7 +- packages/jmespath/tests/unit/pipe.test.ts | 5 + packages/jmespath/tests/unit/slice.test.ts | 5 + packages/jmespath/tests/unit/syntax.test.ts | 5 + packages/jmespath/tests/unit/unicode.test.ts | 5 + packages/jmespath/tests/unit/wildcard.test.ts | 5 + 15 files changed, 970 insertions(+), 4 deletions(-) diff --git a/packages/jmespath/tests/unit/base.test.ts b/packages/jmespath/tests/unit/base.test.ts index dfcea29b66..68c6ad85b9 100644 --- a/packages/jmespath/tests/unit/base.test.ts +++ b/packages/jmespath/tests/unit/base.test.ts @@ -1,3 +1,8 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/base + */ import { search } from '../../src'; describe('Base tests', () => { diff --git a/packages/jmespath/tests/unit/boolean.test.ts b/packages/jmespath/tests/unit/boolean.test.ts index a947a381aa..1d6521fb2b 100644 --- a/packages/jmespath/tests/unit/boolean.test.ts +++ b/packages/jmespath/tests/unit/boolean.test.ts @@ -1,3 +1,8 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/boolean + */ import { search } from '../../src'; describe('Boolean tests', () => { diff --git a/packages/jmespath/tests/unit/current.test.ts b/packages/jmespath/tests/unit/current.test.ts index 4820650f23..67e62b1655 100644 --- a/packages/jmespath/tests/unit/current.test.ts +++ b/packages/jmespath/tests/unit/current.test.ts @@ -1,3 +1,8 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/current + */ import { search } from '../../src'; describe('Current operator tests', () => { diff --git a/packages/jmespath/tests/unit/escape.test.ts b/packages/jmespath/tests/unit/escape.test.ts index 9b3f45c3d5..612843be91 100644 --- a/packages/jmespath/tests/unit/escape.test.ts +++ b/packages/jmespath/tests/unit/escape.test.ts @@ -1,3 +1,8 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/escape + */ import { search } from '../../src'; describe('Escape characters tests', () => { diff --git a/packages/jmespath/tests/unit/filters.test.ts b/packages/jmespath/tests/unit/filters.test.ts index 1a9002f42e..602d2d1700 100644 --- a/packages/jmespath/tests/unit/filters.test.ts +++ b/packages/jmespath/tests/unit/filters.test.ts @@ -1,11 +1,907 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/filters + */ import { search } from '../../src'; describe('Filer operator tests', () => { it.each([ - - ])('should support the current operator', ({ expression, expected }) => { + { + comment: 'Matching a literal', + expression: `foo[?name == 'a']`, + expected: [{ name: 'a' }], + }, + ])('should match a literal', ({ expression, expected }) => { // Prepare - const data = ; + const data = { foo: [{ name: 'a' }, { name: 'b' }] }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: '*[?[0] == `0`]', + expected: [[], []], + }, + ])('should match a literal in arrays', ({ expression, expected }) => { + // Prepare + const data = { foo: [0, 1], bar: [2, 3] }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'foo[?first == last]', + expected: [{ first: 'foo', last: 'foo' }], + }, + { + comment: 'Verify projection created from filter', + expression: 'foo[?first == last].first', + expected: ['foo'], + }, + ])('should match an expression', ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { first: 'foo', last: 'bar' }, + { first: 'foo', last: 'foo' }, + { first: 'foo', last: 'baz' }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + comment: 'Greater than with a number', + expression: 'foo[?age > `25`]', + expected: [{ age: 30 }], + }, + { + expression: 'foo[?age >= `25`]', + expected: [{ age: 25 }, { age: 30 }], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?age > `30`]', + expected: [], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?age < `25`]', + expected: [{ age: 20 }], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?age <= `25`]', + expected: [{ age: 20 }, { age: 25 }], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?age < `20`]', + expected: [], + }, + { + expression: 'foo[?age == `20`]', + expected: [{ age: 20 }], + }, + { + expression: 'foo[?age != `20`]', + expected: [{ age: 25 }, { age: 30 }], + }, + ])( + 'should match an expression with operators', + ({ expression, expected }) => { + // Prepare + const data = { foo: [{ age: 20 }, { age: 25 }, { age: 30 }] }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Greater than with a number', + expression: 'foo[?weight > `44.4`]', + expected: [{ weight: 55.5 }], + }, + { + expression: 'foo[?weight >= `44.4`]', + expected: [{ weight: 44.4 }, { weight: 55.5 }], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?weight > `55.5`]', + expected: [], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?weight < `44.4`]', + expected: [{ weight: 33.3 }], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?weight <= `44.4`]', + expected: [{ weight: 33.3 }, { weight: 44.4 }], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?weight < `33.3`]', + expected: [], + }, + { + expression: 'foo[?weight == `33.3`]', + expected: [{ weight: 33.3 }], + }, + { + expression: 'foo[?weight != `33.3`]', + expected: [{ weight: 44.4 }, { weight: 55.5 }], + }, + ])( + 'should match an expression with comparisons', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [{ weight: 33.3 }, { weight: 44.4 }, { weight: 55.5 }], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: `foo[?top.name == 'a']`, + expected: [{ top: { name: 'a' } }], + }, + ])('should match with subexpression', ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { first: 'foo', last: 'bar' }, + { first: 'foo', last: 'foo' }, + { first: 'foo', last: 'baz' }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + comment: 'Matching an expression', + expression: 'foo[?top.first == top.last]', + expected: [{ top: { first: 'foo', last: 'foo' } }], + }, + { + comment: 'Matching a JSON array', + expression: 'foo[?top == `{"first": "foo", "last": "bar"}`]', + expected: [{ top: { first: 'foo', last: 'bar' } }], + }, + ])('should match with arrays', ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { top: { first: 'foo', last: 'bar' } }, + { top: { first: 'foo', last: 'foo' } }, + { top: { first: 'foo', last: 'baz' } }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'foo[?key == `true`]', + expected: [{ key: true }], + }, + { + expression: 'foo[?key == `false`]', + expected: [{ key: false }], + }, + { + expression: 'foo[?key == `0`]', + expected: [{ key: 0 }], + }, + { + expression: 'foo[?key == `1`]', + expected: [{ key: 1 }], + }, + { + expression: 'foo[?key == `[0]`]', + expected: [{ key: [0] }], + }, + { + expression: 'foo[?key == `{"bar": [0]}`]', + expected: [{ key: { bar: [0] } }], + }, + { + expression: 'foo[?key == `null`]', + expected: [{ key: null }], + }, + { + expression: 'foo[?key == `[1]`]', + expected: [{ key: [1] }], + }, + { + expression: 'foo[?key == `{"a":2}`]', + expected: [{ key: { a: 2 } }], + }, + { + expression: 'foo[?`true` == key]', + expected: [{ key: true }], + }, + { + expression: 'foo[?`false` == key]', + expected: [{ key: false }], + }, + { + expression: 'foo[?`0` == key]', + expected: [{ key: 0 }], + }, + { + expression: 'foo[?`1` == key]', + expected: [{ key: 1 }], + }, + { + expression: 'foo[?`[0]` == key]', + expected: [{ key: [0] }], + }, + { + expression: 'foo[?`{"bar": [0]}` == key]', + expected: [{ key: { bar: [0] } }], + }, + { + expression: 'foo[?`null` == key]', + expected: [{ key: null }], + }, + { + expression: 'foo[?`[1]` == key]', + expected: [{ key: [1] }], + }, + { + expression: 'foo[?`{"a":2}` == key]', + expected: [{ key: { a: 2 } }], + }, + { + expression: 'foo[?key != `true`]', + expected: [ + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?key != `false`]', + expected: [ + { key: true }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?key != `0`]', + expected: [ + { key: true }, + { key: false }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?key != `1`]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?key != `null`]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?key != `[1]`]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?key != `{"a":2}`]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + ], + }, + { + expression: 'foo[?`true` != key]', + expected: [ + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?`false` != key]', + expected: [ + { key: true }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?`0` != key]', + expected: [ + { key: true }, + { key: false }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?`1` != key]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?`null` != key]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?`[1]` != key]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?`{"a":2}` != key]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + ], + }, + ])( + 'should match with object that have mixed types as values', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[?key == `true`]', + expected: [{ key: true }], + }, + { + expression: 'foo[?key == `false`]', + expected: [{ key: false }], + }, + { + expression: 'foo[?key]', + expected: [ + { key: true }, + { key: 0 }, + { key: 0.0 }, + { key: 1 }, + { key: 1.0 }, + { key: [0] }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[? !key]', + expected: [{ key: false }, { key: null }, { key: [] }, { key: {} }], + }, + { + expression: 'foo[? !!key]', + expected: [ + { key: true }, + { key: 0 }, + { key: 0.0 }, + { key: 1 }, + { key: 1.0 }, + { key: [0] }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[? `true`]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 0.0 }, + { key: 1 }, + { key: 1.0 }, + { key: [0] }, + { key: null }, + { key: [1] }, + { key: [] }, + { key: {} }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[? `false`]', + expected: [], + }, + ])('should match with falsy values', ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 0.0 }, + { key: 1 }, + { key: 1.0 }, + { key: [0] }, + { key: null }, + { key: [1] }, + { key: [] }, + { key: {} }, + { key: { a: 2 } }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'reservations[].instances[?bar==`1`]', + expected: [[{ foo: 2, bar: 1 }]], + }, + { + expression: 'reservations[*].instances[?bar==`1`]', + expected: [[{ foo: 2, bar: 1 }]], + }, + { + expression: 'reservations[].instances[?bar==`1`][]', + expected: [{ foo: 2, bar: 1 }], + }, + ])( + 'should match with nested objects and arrays', + ({ expression, expected }) => { + // Prepare + const data = { + reservations: [ + { + instances: [ + { foo: 1, bar: 2 }, + { foo: 1, bar: 3 }, + { foo: 1, bar: 2 }, + { foo: 2, bar: 1 }, + ], + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[?bar==`1`].bar[0]', + expected: [], + }, + ])( + 'should match with nested objects and arrays with different structures', + ({ expression, expected }) => { + // Prepare + const data = { + baz: 'other', + foo: [ + { bar: 1 }, + { bar: 2 }, + { bar: 3 }, + { bar: 4 }, + { bar: 1, baz: 2 }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[?a==`1`].b.c', + expected: ['x', 'y', 'z'], + }, + ])('should support filter in indexes', ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { a: 1, b: { c: 'x' } }, + { a: 1, b: { c: 'y' } }, + { a: 1, b: { c: 'z' } }, + { a: 2, b: { c: 'z' } }, + { a: 1, baz: 2 }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + comment: 'Filter with or expression', + expression: `foo[?name == 'a' || name == 'b']`, + expected: [{ name: 'a' }, { name: 'b' }], + }, + { + expression: `foo[?name == 'a' || name == 'e']`, + expected: [{ name: 'a' }], + }, + { + expression: `foo[?name == 'a' || name == 'b' || name == 'c']`, + expected: [{ name: 'a' }, { name: 'b' }, { name: 'c' }], + }, + ])( + 'should support filter with or expressions', + ({ expression, expected }) => { + // Prepare + const data = { foo: [{ name: 'a' }, { name: 'b' }, { name: 'c' }] }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Filter with and expression', + expression: 'foo[?a == `1` && b == `2`]', + expected: [{ a: 1, b: 2 }], + }, + { + expression: 'foo[?a == `1` && b == `4`]', + expected: [], + }, + ])('should support filter and expressions', ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { a: 1, b: 2 }, + { a: 1, b: 3 }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + comment: 'Filter with Or and And expressions', + expression: 'foo[?c == `3` || a == `1` && b == `4`]', + expected: [{ a: 1, b: 2, c: 3 }], + }, + { + expression: 'foo[?b == `2` || a == `3` && b == `4`]', + expected: [ + { a: 1, b: 2, c: 3 }, + { a: 3, b: 4 }, + ], + }, + { + expression: 'foo[?a == `3` && b == `4` || b == `2`]', + expected: [ + { a: 1, b: 2, c: 3 }, + { a: 3, b: 4 }, + ], + }, + { + expression: 'foo[?(a == `3` && b == `4`) || b == `2`]', + expected: [ + { a: 1, b: 2, c: 3 }, + { a: 3, b: 4 }, + ], + }, + { + expression: 'foo[?((a == `3` && b == `4`)) || b == `2`]', + expected: [ + { a: 1, b: 2, c: 3 }, + { a: 3, b: 4 }, + ], + }, + { + expression: 'foo[?a == `3` && (b == `4` || b == `2`)]', + expected: [{ a: 3, b: 4 }], + }, + { + expression: 'foo[?a == `3` && ((b == `4` || b == `2`))]', + expected: [{ a: 3, b: 4 }], + }, + ])( + 'should support filter with or & and expressions', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { a: 1, b: 2, c: 3 }, + { a: 3, b: 4 }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Verify precedence of or/and expressions', + expression: 'foo[?a == `1` || b ==`2` && c == `5`]', + expected: [{ a: 1, b: 2, c: 3 }], + }, + { + comment: 'Parentheses can alter precedence', + expression: 'foo[?(a == `1` || b ==`2`) && c == `5`]', + expected: [], + }, + { + comment: 'Not expressions combined with and/or', + expression: 'foo[?!(a == `1` || b ==`2`)]', + expected: [{ a: 3, b: 4 }], + }, + ])( + 'should support filter with expressions and respect precedence', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { a: 1, b: 2, c: 3 }, + { a: 3, b: 4 }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Unary filter expression', + expression: 'foo[?key]', + expected: [ + { key: true }, + { key: [0] }, + { key: { a: 'b' } }, + { key: 0 }, + { key: 1 }, + ], + }, + { + comment: 'Unary not filter expression', + expression: 'foo[?!key]', + expected: [ + { key: false }, + { key: [] }, + { key: {} }, + { key: null }, + { notkey: true }, + ], + }, + { + comment: 'Equality with null RHS', + expression: 'foo[?key == `null`]', + expected: [{ key: null }, { notkey: true }], + }, + ])('should support unary expressions', ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { key: true }, + { key: false }, + { key: [] }, + { key: {} }, + { key: [0] }, + { key: { a: 'b' } }, + { key: 0 }, + { key: 1 }, + { key: null }, + { notkey: true }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + comment: 'Using @ in a filter expression', + expression: 'foo[?@ < `5`]', + expected: [0, 1, 2, 3, 4], + }, + { + comment: 'Using @ in a filter expression', + expression: 'foo[?`5` > @]', + expected: [0, 1, 2, 3, 4], + }, + { + comment: 'Using @ in a filter expression', + expression: 'foo[?@ == @]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + ])('should support using current in a filter', ({ expression, expected }) => { + // Prepare + const data = { + foo: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }; // Act const result = search(expression, data); diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/functions.test.ts index db9735dc3e..166201a1cf 100644 --- a/packages/jmespath/tests/unit/functions.test.ts +++ b/packages/jmespath/tests/unit/functions.test.ts @@ -1,3 +1,8 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/functions + */ import { search } from '../../src'; describe('Functions tests', () => { diff --git a/packages/jmespath/tests/unit/identifiers.test.ts b/packages/jmespath/tests/unit/identifiers.test.ts index 7ac452736a..993e26ab8e 100644 --- a/packages/jmespath/tests/unit/identifiers.test.ts +++ b/packages/jmespath/tests/unit/identifiers.test.ts @@ -1,3 +1,8 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/identifiers + */ import { search } from '../../src'; describe('Identifiers tests', () => { diff --git a/packages/jmespath/tests/unit/indices.test.ts b/packages/jmespath/tests/unit/indices.test.ts index 367fc6feb5..8a8e16a549 100644 --- a/packages/jmespath/tests/unit/indices.test.ts +++ b/packages/jmespath/tests/unit/indices.test.ts @@ -1,3 +1,8 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/indices + */ import { search } from '../../src'; describe('Indices tests', () => { diff --git a/packages/jmespath/tests/unit/literal.test.ts b/packages/jmespath/tests/unit/literal.test.ts index 53382bcd90..8036ad544a 100644 --- a/packages/jmespath/tests/unit/literal.test.ts +++ b/packages/jmespath/tests/unit/literal.test.ts @@ -1,3 +1,8 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/literal + */ import { search } from '../../src'; describe('Literal expressions tests', () => { diff --git a/packages/jmespath/tests/unit/multiselect.test.ts b/packages/jmespath/tests/unit/multiselect.test.ts index 28c3e2f4a6..0a969f6b4b 100644 --- a/packages/jmespath/tests/unit/multiselect.test.ts +++ b/packages/jmespath/tests/unit/multiselect.test.ts @@ -1,3 +1,8 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/multiselect + */ import { search } from '../../src'; describe('Multiselect expressions tests', () => { @@ -563,7 +568,7 @@ describe('Multiselect expressions tests', () => { 'should handle nested multiselect with empty arrays', ({ expression, expected }) => { // Prepare - const data = []; + const data: string[] = []; // Act const result = search(expression, data); diff --git a/packages/jmespath/tests/unit/pipe.test.ts b/packages/jmespath/tests/unit/pipe.test.ts index ceed41d2f4..b311eb3352 100644 --- a/packages/jmespath/tests/unit/pipe.test.ts +++ b/packages/jmespath/tests/unit/pipe.test.ts @@ -1,3 +1,8 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/pipe + */ import { search } from '../../src'; describe('Pipe expressions tests', () => { diff --git a/packages/jmespath/tests/unit/slice.test.ts b/packages/jmespath/tests/unit/slice.test.ts index 2005f004ba..c18daedecd 100644 --- a/packages/jmespath/tests/unit/slice.test.ts +++ b/packages/jmespath/tests/unit/slice.test.ts @@ -1,3 +1,8 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/slice + */ import { search } from '../../src'; describe('Slices tests', () => { diff --git a/packages/jmespath/tests/unit/syntax.test.ts b/packages/jmespath/tests/unit/syntax.test.ts index b0dd71cb19..e729669658 100644 --- a/packages/jmespath/tests/unit/syntax.test.ts +++ b/packages/jmespath/tests/unit/syntax.test.ts @@ -1,3 +1,8 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/syntax + */ import { search } from '../../src'; describe('Syntax tests', () => { diff --git a/packages/jmespath/tests/unit/unicode.test.ts b/packages/jmespath/tests/unit/unicode.test.ts index 8a03dffe3d..6c07fb3e4a 100644 --- a/packages/jmespath/tests/unit/unicode.test.ts +++ b/packages/jmespath/tests/unit/unicode.test.ts @@ -1,3 +1,8 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/unicode + */ import { search } from '../../src'; describe('Unicode tests', () => { diff --git a/packages/jmespath/tests/unit/wildcard.test.ts b/packages/jmespath/tests/unit/wildcard.test.ts index 98de5b20ef..eb46d1147b 100644 --- a/packages/jmespath/tests/unit/wildcard.test.ts +++ b/packages/jmespath/tests/unit/wildcard.test.ts @@ -1,3 +1,8 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/wildcard + */ import { search } from '../../src'; describe('Wildcard tests', () => { From 46a0656bf3cb5a014ed8a752a98752102ddd4f88 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 6 Jun 2023 18:40:00 +0000 Subject: [PATCH 052/103] tests: 68% compliance with spec --- packages/jmespath/src/Lexer.ts | 3 - packages/jmespath/src/Parser.ts | 16 +- packages/jmespath/src/errors.ts | 65 +++- packages/jmespath/src/types/JSON.ts | 2 +- .../jmespath/src/visitor/TreeInterpreter.ts | 95 ++++- packages/jmespath/src/visitor/utils.ts | 20 +- packages/jmespath/tests/unit/base.test.ts | 98 ++++-- packages/jmespath/tests/unit/boolean.test.ts | 5 + packages/jmespath/tests/unit/syntax.test.ts | 329 ++++++++++-------- 9 files changed, 418 insertions(+), 215 deletions(-) diff --git a/packages/jmespath/src/Lexer.ts b/packages/jmespath/src/Lexer.ts index 88a5d35931..3f2bfd3eff 100644 --- a/packages/jmespath/src/Lexer.ts +++ b/packages/jmespath/src/Lexer.ts @@ -85,7 +85,6 @@ class Lexer { end: start + buff.length, }; } else { - // TODO: see if we can pass the error message `"Unknown token '%s'" % buff` to the LexerError throw new LexerError(start, buff); } } else if (this.#current === '"') { @@ -116,11 +115,9 @@ class Lexer { } else { position = this.#position - 1; } - // TODO: see if we can pass a message "Unknown token '='" to LexerError throw new LexerError(position, '='); } } else { - // TODO: see if we can pass a message `Unknown token ${this.#current}` to LexerError throw new LexerError(this.#position, this.#current); } } diff --git a/packages/jmespath/src/Parser.ts b/packages/jmespath/src/Parser.ts index 0308758cae..67e8e4090c 100644 --- a/packages/jmespath/src/Parser.ts +++ b/packages/jmespath/src/Parser.ts @@ -104,14 +104,12 @@ class Parser { try { return this.#parse(expression); } catch (error) { - if (error instanceof LexerError) { - error.expression = expression; - throw error; - } else if (error instanceof IncompleteExpressionError) { - error.expression = expression; - throw error; - } else if (error instanceof ParseError) { - error.expression = expression; + if ( + error instanceof LexerError || + error instanceof IncompleteExpressionError || + error instanceof ParseError + ) { + error.setExpression(expression); throw error; } else { throw error; @@ -580,7 +578,7 @@ class Parser { lexPosition: token.start, tokenValue: token.value, tokenType: token.type, - reason: `Expecting: ${allowed.join(', ')}, got: ${token.type}`, + reason: `Expecting one of: ${allowed.join(', ')}, got: ${token.type}`, }); } } diff --git a/packages/jmespath/src/errors.ts b/packages/jmespath/src/errors.ts index 460a846361..ec471a91e0 100644 --- a/packages/jmespath/src/errors.ts +++ b/packages/jmespath/src/errors.ts @@ -4,23 +4,47 @@ import type { Token } from './types'; * TODO: write docs for JMESPathError */ class JMESPathError extends Error { + /** + * Expression that was being parsed when the error occurred. + * Can be set by whatever catches the error. + */ + public expression?: string; + public constructor(message: string) { super(message); this.name = 'JMESPathError'; + this.message = message; + } + + /** + * Set the expression that was being parsed when the error occurred. + * + * The separate method allows the expression to be set after the error is + * thrown. In some instances the expression is not known until after the + * error is thrown (i.e. the error is thrown down the call stack). + * + * @param expression The expression that was being parsed when the error occurred. + */ + public setExpression(expression: string): void { + this.expression = expression; + + // Set the message to include the expression. + this.message = `${this.message} for expression: ${this.expression}`; } } /** - * TODO: write docs for LexerError + * Error thrown when an unknown token is encountered during the AST construction. + * TODO: improve field names for LexerError */ class LexerError extends JMESPathError { /** - * Expression that was being parsed when the error occurred. - * - * Can be set by whatever catches the error. + * Position in the expression where the error occurred. */ - public expression?: string; public lexerPosition: number; + /** + * Token value where the error occurred. + */ public lexerValue: string; public constructor(lexerPosition: number, lexerValue: string) { @@ -30,25 +54,30 @@ class LexerError extends JMESPathError { this.lexerValue = lexerValue; // Set the message to include the lexer position and value. - this.message = `${super.message}: ${this.expression} at position ${ - this.lexerPosition - }: ${this.lexerValue}`; + this.message = `${this.message}: unknown token "${this.lexerValue}" at column ${this.lexerPosition}`; } } /** - * TODO: write docs for ParseError + * Error thrown when an invalid or unexpected token type or value is encountered during parsing. + * TODO: improve field names for ParseError */ class ParseError extends JMESPathError { /** - * Expression that was being parsed when the error occurred. - * - * Can be set by whatever catches the error. + * Position in the expression where the error occurred. */ - public expression?: string; public lexPosition: number; + /** + * Additional information about the error. + */ public reason?: string; + /** + * Token type where the error occurred. + */ public tokenType: Token['type']; + /** + * Token value where the error occurred. + */ public tokenValue: Token['value']; public constructor(options: { @@ -65,11 +94,11 @@ class ParseError extends JMESPathError { this.reason = options.reason; // Set the message to include the lexer position and token info. - this.message = `${super.message}: ${this.reason}\nParse error at column ${ - this.lexPosition - }, token "${this.tokenValue}" (${this.tokenType}), for expression:\n${ - this.expression - }`; + const issue = + this.tokenType === 'eof' + ? 'found unexpected end of expression (EOF)' + : `found unexpected token "${this.tokenValue}" (${this.tokenType})`; + this.message = `${this.message}: parse error at column ${this.lexPosition}, ${issue}`; } } diff --git a/packages/jmespath/src/types/JSON.ts b/packages/jmespath/src/types/JSON.ts index 036bc47254..2a44a5ba99 100644 --- a/packages/jmespath/src/types/JSON.ts +++ b/packages/jmespath/src/types/JSON.ts @@ -1,4 +1,4 @@ -type JSONPrimitive = string | number | boolean | null; +type JSONPrimitive = string | number | boolean | null | undefined; type JSONValue = JSONPrimitive | JSONObject | JSONArray; type JSONObject = { [key: string]: JSONValue }; type JSONArray = Array; diff --git a/packages/jmespath/src/visitor/TreeInterpreter.ts b/packages/jmespath/src/visitor/TreeInterpreter.ts index 6c6b091dd1..319064bb25 100644 --- a/packages/jmespath/src/visitor/TreeInterpreter.ts +++ b/packages/jmespath/src/visitor/TreeInterpreter.ts @@ -24,14 +24,14 @@ class TreeInterpreter { * @param value * @returns */ - public visit(node: Node, value: JSONValue): JSONValue { + public visit(node: Node, value: JSONValue): JSONValue | undefined { const nodeType = node.type; if (nodeType === 'subexpression') { return this.#visitSubexpression(node, value); } else if (nodeType === 'field') { return this.#visitField(node, value); - /* } else if (nodeType === 'comparator') { - return this.#visitComparator(node, value); */ + } else if (nodeType === 'comparator') { + return this.#visitComparator(node, value); } else if (nodeType === 'current') { return this.#visitCurrent(node, value); } else if (nodeType === 'expref') { @@ -65,14 +65,14 @@ class TreeInterpreter { return this.#visitOrExpression(node, value); } else if (nodeType === 'and_expression') { return this.#visitAndExpression(node, value); - /* } else if (nodeType === 'not_expression') { - return this.#visitNotExpression(node, value); */ + } else if (nodeType === 'not_expression') { + return this.#visitNotExpression(node, value); } else if (nodeType === 'pipe') { return this.#visitPipe(node, value); } else if (nodeType === 'projection') { return this.#visitProjection(node, value); - /* } else if (nodeType === 'value_projection') { - return this.#visitValueProjection(node, value); */ + } else if (nodeType === 'value_projection') { + return this.#visitValueProjection(node, value); } else { // TODO: convert to a custom error throw new Error(`Not Implemented: Invalid node type: ${node.type}`); @@ -119,10 +119,39 @@ class TreeInterpreter { * @param value * @returns */ - /* #visitComparator(node: Node, value: JSONValue): JSONValue { - return true; + #visitComparator(node: Node, value: JSONValue): JSONValue { + const comparator = node.value; + const left = this.visit(node.children[0], value); + const right = this.visit(node.children[1], value); + if ( + typeof comparator === 'string' && + ['eq', 'ne', 'gt', 'gte', 'lt', 'lte'].includes(comparator) + ) { + // Common cases: comparator is == or != + if (comparator === 'eq') { + return left === right; + } else if (comparator === 'ne') { + return left !== right; + } else if (typeof left === 'number' && typeof right === 'number') { + // Ordering operators only work on numbers. Evaluating them on other + // types will return null. + if (comparator === 'lt') { + return left < right; + } else if (comparator === 'lte') { + return left <= right; + } else if (comparator === 'gt') { + return left > right; + } else { + return left >= right; + } + } else { + return null; + } + } else { + // TODO: make invalid comparator a custom error + throw new Error(`Invalid comparator: ${comparator}`); + } } - */ /** * TODO: write docs for TreeInterpreter.visitCurrent() @@ -155,15 +184,23 @@ class TreeInterpreter { for (const child of node.children) { args.push(this.visit(child, value)); } + // check that method name is a string + if (typeof node.value !== 'string') { + throw new Error(`Function name must be a string, got ${node.value}`); + } const methods = Object.getOwnPropertyNames( Object.getPrototypeOf(this.#functions) ); + // convert snake_case to camelCase + const normalizedFunctionName = node.value.replace(/_([a-z])/g, (g) => + g[1].toUpperCase() + ); const methodName = methods.find( - (method) => method.replace('func', '').toLowerCase() === node.value + (method) => method === `func${normalizedFunctionName}` ); if (!methodName) { // TODO: convert to a custom error - throw new Error(`Function not found: ${methodName}`); + throw new Error(`Function not found: ${node.value}`); } // We know that methodName is a key of this.#functions, but TypeScript @@ -349,10 +386,10 @@ class TreeInterpreter { #visitOrExpression(node: Node, value: JSONValue): JSONValue { const matched = this.visit(node.children[0], value); if (!isTruthy(matched)) { - return matched; + return this.visit(node.children[1], value); } - return this.visit(node.children[1], value); + return matched; } /** @@ -376,9 +413,16 @@ class TreeInterpreter { * @param value * @returns */ - /* #visitNotExpression(node: Node, value: JSONValue): JSONValue { - return true; - } */ + #visitNotExpression(node: Node, value: JSONValue): JSONValue { + const originalResult = this.visit(node.children[0], value); + if (typeof originalResult === 'number' && originalResult === 0) { + // Special case for 0, !0 should be false, not true. + // 0 is not a special cased integer in jmespath. + return false; + } + + return !isTruthy(originalResult); + } /** * TODO: write docs for TreeInterpreter.visitPipe() @@ -423,9 +467,22 @@ class TreeInterpreter { * @param value * @returns */ - /* #visitValueProjection(node: Node, value: JSONValue): JSONValue { + #visitValueProjection(node: Node, value: JSONValue): JSONValue { const base = this.visit(node.children[0], value); - } */ + if (!isRecord(base)) { + return null; + } + const values = Object.values(base); + const collected = []; + for (const item of values) { + const current = this.visit(node.children[1], item); + if (current !== null) { + collected.push(current); + } + } + + return collected; + } } export { TreeInterpreter }; diff --git a/packages/jmespath/src/visitor/utils.ts b/packages/jmespath/src/visitor/utils.ts index 88a54684b0..8c74a7dca4 100644 --- a/packages/jmespath/src/visitor/utils.ts +++ b/packages/jmespath/src/visitor/utils.ts @@ -38,13 +38,19 @@ const isRecord = (value: unknown): value is Record => { * @returns */ const isTruthy = (value: unknown): boolean => { - return ( - value !== '' || - (Array.isArray(value) && value.length > 0) || - (isRecord(value) && Object(value).keys().length > 0) || - value !== undefined || - Object.is(value, true) - ); + if (typeof value === 'string') { + return value !== ''; + } else if (typeof value === 'number') { + return value !== 0; + } else if (typeof value === 'boolean') { + return value; + } else if (Array.isArray(value)) { + return value.length > 0; + } else if (isRecord(value)) { + return Object.keys(value).length > 0; + } else { + return Object.is(value, true); + } }; export { Expression, isRecord, isTruthy }; diff --git a/packages/jmespath/tests/unit/base.test.ts b/packages/jmespath/tests/unit/base.test.ts index 68c6ad85b9..f4942363fc 100644 --- a/packages/jmespath/tests/unit/base.test.ts +++ b/packages/jmespath/tests/unit/base.test.ts @@ -7,17 +7,45 @@ import { search } from '../../src'; describe('Base tests', () => { it.each([ - { expression: 'foo', expected: { bar: { baz: 'qux' } } }, - { expression: 'foo.bar', expected: { baz: 'qux' } }, - { expression: 'foo.bar.baz', expected: 'qux' }, - { expression: 'foo.bar.baz.qux', expected: undefined }, - { expression: 'qux', expected: undefined }, - { expression: 'qux.quux', expected: undefined }, - { expression: 'qux.quux.quuux', expected: undefined }, - { expression: 'ffoo\n.\nbar\n.baz', expected: 'qux' }, + { + expression: 'foo', + expected: { bar: { baz: 'correct' } }, + }, + { + expression: 'foo.bar', + expected: { baz: 'correct' }, + }, + { + expression: 'foo.bar.baz', + expected: 'correct', + }, + { + expression: 'foo\n.\nbar\n.baz', + expected: 'correct', + }, + { + expression: 'foo.bar.baz.bad', + expected: null, + }, + { + expression: 'foo.bar.bad', + expected: null, + }, + { + expression: 'foo.bad', + expected: null, + }, + { + expression: 'bad', + expected: null, + }, + { + expression: 'bad.morebad.morebad', + expected: null, + }, ])('should parse a multi-level nested object', ({ expression, expected }) => { // Prepare - const data = { foo: { bar: { baz: 'qux' } } }; + const data = { foo: { bar: { baz: 'correct' } } }; // Act const result = search(expression, data); @@ -27,14 +55,19 @@ describe('Base tests', () => { }); it.each([ - { expression: 'foo', expected: { bar: ['a', 'b', 'c'] } }, - { expression: 'foo.bar', expected: ['a', 'b', 'c'] }, - { expression: 'foo.bar.a', expected: undefined }, + { + expression: 'foo', + expected: { bar: ['one', 'two', 'three'] }, + }, + { + expression: 'foo.bar', + expected: ['one', 'two', 'three'], + }, ])( 'should parse multi-level objects with arrays', ({ expression, expected }) => { // Prepare - const data = { foo: { bar: ['a', 'b', 'c'] } }; + const data = { foo: { bar: ['one', 'two', 'three'] } }; // Act const result = search(expression, data); @@ -45,13 +78,25 @@ describe('Base tests', () => { ); it.each([ - { expression: 'a', expected: undefined }, - { expression: 'b', expected: undefined }, - { expression: 'c', expected: undefined }, - { expression: 'a.b', expected: undefined }, + { + expression: 'one', + expected: null, + }, + { + expression: 'two', + expected: null, + }, + { + expression: 'three', + expected: null, + }, + { + expression: 'one.two', + expected: null, + }, ])('should parse an array', ({ expression, expected }) => { // Prepare - const data = ['a', 'b', 'c']; + const data = ['one', 'two', 'three']; // Act const result = search(expression, data); @@ -61,14 +106,23 @@ describe('Base tests', () => { }); it.each([ - { expression: 'foo."1"', expected: ['a', 'b', 'c'] }, - { expression: 'foo."1"[0]', expected: 'a' }, - { expression: 'foo."-1"', expected: 'bar' }, + { + expression: 'foo."1"', + expected: ['one', 'two', 'three'], + }, + { + expression: 'foo."1"[0]', + expected: 'one', + }, + { + expression: 'foo."-1"', + expected: 'bar', + }, ])( 'should parse an object with arrays and numeric values as keys', ({ expression, expected }) => { // Prepare - const data = { foo: { '1': ['a', 'b', 'c'], '-1': 'bar' } }; + const data = { foo: { '1': ['one', 'two', 'three'], '-1': 'bar' } }; // Act const result = search(expression, data); diff --git a/packages/jmespath/tests/unit/boolean.test.ts b/packages/jmespath/tests/unit/boolean.test.ts index 1d6521fb2b..55856b4ff9 100644 --- a/packages/jmespath/tests/unit/boolean.test.ts +++ b/packages/jmespath/tests/unit/boolean.test.ts @@ -69,6 +69,11 @@ describe('Boolean tests', () => { expression: 'outer.empty_string || outer.foo', expected: 'foo', }, + { + expression: + 'outer.nokey || outer.bool || outer.empty_list || outer.empty_string || outer.foo', + expected: 'foo', + }, ])( 'should support multiple boolean OR comparisons', ({ expression, expected }) => { diff --git a/packages/jmespath/tests/unit/syntax.test.ts b/packages/jmespath/tests/unit/syntax.test.ts index e729669658..15c1c4c975 100644 --- a/packages/jmespath/tests/unit/syntax.test.ts +++ b/packages/jmespath/tests/unit/syntax.test.ts @@ -11,143 +11,141 @@ describe('Syntax tests', () => { expression: 'foo.bar', expected: null, }, - { - expression: 'foo.1', - error: 'Syntax error, unexpected token: 1(Number)', - }, - { - expression: 'foo.-11', - error: 'Syntax error, unexpected token: -11(Number)', - }, { expression: 'foo', expected: null, }, - { - expression: 'foo.', - error: 'Syntax error, unexpected token: (EOF)', - }, - { - expression: 'foo.', - error: 'Syntax error, unexpected token: (EOF)', - }, - { - expression: '.foo', - error: 'Invalid token (Dot): "."', - }, - { - expression: 'foo..bar', - error: 'Syntax error, unexpected token: .(Dot)', - }, - { - expression: 'foo.bar.', - error: 'Syntax error, unexpected token: (EOF)', - }, - { - expression: 'foo[.]', - error: 'Expected Star, got: Dot', - }, - ])('should support dot syntax', ({ expression, error }) => { + ])('should support dot syntax', ({ expression, expected }) => { // Prepare const data = { type: 'object', }; - // Act & Assess - expect(() => search(expression, data)).toThrow(error); + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); }); it.each([ { expression: 'foo.1', - error: 'Syntax error, unexpected token: 1(Number)', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "1" (number) for expression: foo.1', }, { expression: 'foo.-11', - error: 'Syntax error, unexpected token: -11(Number)', - }, - { - expression: 'foo.', - error: 'Syntax error, unexpected token: (EOF)', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "-11" (number) for expression: foo.-11', }, { expression: 'foo.', - error: 'Syntax error, unexpected token: (EOF)', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected end of expression (EOF) for expression: foo.', }, { expression: '.foo', - error: 'Invalid token (Dot): "."', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token "." (dot) for expression: .foo', }, { expression: 'foo..bar', - error: 'Syntax error, unexpected token: .(Dot)', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "." (dot) for expression: foo..bar', }, { expression: 'foo.bar.', - error: 'Syntax error, unexpected token: (EOF)', + error: + 'Invalid jmespath expression: parse error at column 8, found unexpected end of expression (EOF) for expression: foo.', }, { expression: 'foo[.]', - error: 'Expected Star, got: Dot', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "." (dot) for expression: foo[.]', }, + ])('dot syntax errors', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ { expression: '.', - error: 'Invalid token (Dot): "."', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token "." (dot) for expression: .', }, { expression: ':', - error: 'Invalid token (Colon): ":"', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token ":" (colon) for expression: :', }, { expression: ',', - error: 'Invalid token (Comma): ","', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token "," (comma) for expression: ,', }, { expression: ']', - error: 'Invalid token (Rbracket): "]"', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token "]" (rbracket) for expression: ]', }, { expression: '[', - error: 'Invalid token (EOF): ""', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) for expression: [', }, { expression: '}', - error: 'Invalid token (Rbrace): "}"', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token "}" (rbrace) for expression: }', }, { expression: '{', - error: 'Expecting an identifier token, got: EOF', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) for expression: {', }, { expression: ')', - error: 'Invalid token (Rparen): ")"', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token ")" (rparen) for expression: )', }, { expression: '(', - error: 'Invalid token (EOF): ""', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) for expression: (', }, { expression: '((&', - error: 'Invalid token (EOF): ""', + error: + 'Invalid jmespath expression: parse error at column 3, found unexpected end of expression (EOF) for expression: ((&', }, { expression: 'a[', - error: 'Expected Star, got: EOF', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected end of expression (EOF) for expression: a[', }, { expression: 'a]', - error: 'Unexpected token type: Rbracket, value: ]', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected token "]" (rbracket) for expression: a]', }, { expression: 'a][', - error: 'Unexpected token type: Rbracket, value: ]', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected token "]" (rbracket) for expression: a]', }, { expression: '!', - error: 'Invalid token (EOF): ""', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) for expression: !', }, ])('simple token errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in simple token errors tests // Prepare const data = { type: 'object', @@ -160,10 +158,10 @@ describe('Syntax tests', () => { it.each([ { expression: '![!(!', - error: 'Invalid token (EOF): ""', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected end of expression (EOF) for expression: ![!(!', }, ])('boolean token errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in boolean token errors tests // Prepare const data = { type: 'object', @@ -190,7 +188,7 @@ describe('Syntax tests', () => { expression: '*[0]', expected: [], }, - ])('shoudl support wildcard syntax', ({ expression, expected }) => { + ])('should support wildcard syntax', ({ expression, expected }) => { // Prepare const data = { type: 'object', @@ -206,26 +204,30 @@ describe('Syntax tests', () => { it.each([ { expression: '.*', - error: 'Invalid token (Dot): "."', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token "." (dot) for expression: .*', }, { expression: '*foo', - error: 'Unexpected token type: UnquotedIdentifier, value: foo', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected token "foo" (unquoted_identifier) for expression: *foo', }, { expression: '*0', - error: 'Unexpected token type: Number, value: 0', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected token "0" (number) for expression: *0', }, { expression: 'foo[*]bar', - error: 'Unexpected token type: UnquotedIdentifier, value: bar', + error: + 'Invalid jmespath expression: parse error at column 6, found unexpected token "bar" (unquoted_identifier) for expression: foo[*]bar', }, { expression: 'foo[*]*', - error: 'Syntax error, unexpected token: *(Star)', + error: + 'Invalid jmespath expression: parse error at column 6, found unexpected token "*" (star) for expression: foo[*]*', }, ])('wildcard token errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in wildcard token errors tests // Prepare const data = { type: 'object', @@ -290,14 +292,15 @@ describe('Syntax tests', () => { it.each([ { expression: '*.[0]', - error: 'Invalid token (Number): "0"', + error: + 'Invalid jmespath expression: parse error at column 3, found unexpected token "0" (number) for expression: *.[0]', }, { expression: 'foo[#]', - error: 'Unknown character: #', + error: + 'Bad jmespath expression: unknown token "#" at column 4 for expression: foo[#]', }, ])('simple breacket errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in simple bracket errors tests // Prepare const data = { type: 'object', @@ -343,75 +346,88 @@ describe('Syntax tests', () => { { comment: 'Valid multi-select of a list', expression: 'foo[0, 1]', - error: 'Expected Rbracket, got: Comma', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) for expression: foo[0, 1]', }, { expression: 'foo.[0]', - error: 'Invalid token (Number): "0"', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected token "0" (number) for expression: foo.[0]', }, { comment: 'Multi-select of a list with trailing comma', expression: 'foo[0, ]', - error: 'Expected Rbracket, got: Comma', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) for expression: foo[0, ]', }, { comment: 'Multi-select of a list with trailing comma and no close', expression: 'foo[0,', - error: 'Expected Rbracket, got: Comma', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) for expression: foo[0,', }, { comment: 'Multi-select of a list with trailing comma and no close', expression: 'foo.[a', - error: 'Invalid token (EOF): ""', + error: + 'Invalid jmespath expression: parse error at column 6, found unexpected end of expression (EOF) for expression: foo.[a', }, { comment: 'Multi-select of a list with extra comma', expression: 'foo[0,, 1]', - error: 'Expected Rbracket, got: Comma', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) for expression: foo[0,, 1]', }, { comment: 'Multi-select of a list using an identifier index', expression: 'foo[abc]', - error: 'Expected Star, got: UnquotedIdentifier', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) for expression: foo[abc]', }, { comment: 'Multi-select of a list using identifier indices', expression: 'foo[abc, def]', - error: 'Expected Star, got: UnquotedIdentifier', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) for expression: foo[abc, def]', }, { comment: 'Multi-select of a list using an identifier index', expression: 'foo[abc, 1]', - error: 'Expected Star, got: UnquotedIdentifier', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) for expression: foo[abc, 1]', }, { comment: 'Multi-select of a list using an identifier index with trailing comma', expression: 'foo[abc, ]', - error: 'Expected Star, got: UnquotedIdentifier', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) for expression: foo[abc, ]', }, { comment: 'Multi-select of a hash using a numeric index', expression: 'foo.[abc, 1]', - error: 'Invalid token (Number): "1"', + error: + 'Invalid jmespath expression: parse error at column 10, found unexpected token "1" (number) for expression: foo.[abc, 1]', }, { comment: 'Multi-select of a hash with a trailing comma', expression: 'foo.[abc, ]', - error: 'Unexpected token Rbracket', + error: + 'Invalid jmespath expression: parse error at column 10, found unexpected token "]" (rbracket) for expression: foo.[abc, ]', }, { comment: 'Multi-select of a hash with extra commas', expression: 'foo.[abc,, def]', - error: 'Invalid token (Comma): ","', + error: + 'Invalid jmespath expression: parse error at column 9, found unexpected token "," (comma) for expression: foo.[abc,, def]', }, { comment: 'Multi-select of a hash using number indices', expression: 'foo.[0, 1]', - error: 'Invalid token (Number): "0"', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected token "0" (number) for expression: foo.[0, 1]', }, ])('multi-select list errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in multi-select list errors tests // Prepare const data = { type: 'object', @@ -456,94 +472,111 @@ describe('Syntax tests', () => { { comment: 'No key or value', expression: 'a{}', - error: 'Invalid token (Rbrace): "}"', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "}" (rbrace) for expression: a{}', }, { comment: 'No closing token', expression: 'a{', - error: 'Invalid token (EOF): ""', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected end of expression (EOF) for expression: a{', }, { comment: 'Not a key value pair', expression: 'a{foo}', - error: 'Invalid token (UnquotedIdentifier): "foo"', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo}', }, { comment: 'Missing value and closing character', expression: 'a{foo:', - error: 'Invalid token (UnquotedIdentifier): "foo"', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo:', }, { comment: 'Missing closing character', expression: 'a{foo: 0', - error: 'Invalid token (UnquotedIdentifier): "foo"', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo: 0', }, { comment: 'Missing value', expression: 'a{foo:}', - error: 'Invalid token (UnquotedIdentifier): "foo"', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo:}', }, { comment: 'Trailing comma and no closing character', expression: 'a{foo: 0, ', - error: 'Invalid token (UnquotedIdentifier): "foo"', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo: 0, ', }, { comment: 'Missing value with trailing comma', expression: 'a{foo: ,}', - error: 'Invalid token (UnquotedIdentifier): "foo"', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo: ,}', }, { comment: 'Accessing Array using an identifier', expression: 'a{foo: bar}', - error: 'Invalid token (UnquotedIdentifier): "foo"', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo: bar}', }, { expression: 'a{foo: 0}', - error: 'Invalid token (UnquotedIdentifier): "foo"', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo: 0}', }, { comment: 'Missing key-value pair', expression: 'a.{}', - error: 'Expecting an identifier token, got: Rbrace', + error: + 'Invalid jmespath expression: parse error at column 3, found unexpected token "}" (rbrace) for expression: a.{}', }, { comment: 'Not a key-value pair', expression: 'a.{foo}', - error: 'Expected Colon, got: Rbrace', + error: + 'Invalid jmespath expression: parse error at column 6, found unexpected token "}" (rbrace) for expression: a.{foo}', }, { comment: 'Missing value', expression: 'a.{foo:}', - error: 'Invalid token (Rbrace): "}"', + error: + 'Invalid jmespath expression: parse error at column 7, found unexpected token "}" (rbrace) for expression: a.{foo:}', }, { comment: 'Missing value with trailing comma', expression: 'a.{foo: ,}', - error: 'Invalid token (Comma): ","', + error: + 'Invalid jmespath expression: parse error at column 8, found unexpected token "," (comma) for expression: a.{foo: ,}', }, { comment: 'Trailing comma', expression: 'a.{foo: bar, }', - error: 'Expecting an identifier token, got: Rbrace', + error: + 'Invalid jmespath expression: parse error at column 13, found unexpected token "}" (rbrace) for expression: a.{foo: bar, }', }, { comment: 'Missing key in second key-value pair', expression: 'a.{foo: bar, baz}', - error: 'Expected Colon, got: Rbrace', + error: + 'Invalid jmespath expression: parse error at column 16, found unexpected token "}" (rbrace) for expression: a.{foo: bar, baz}', }, { comment: 'Missing value in second key-value pair', expression: 'a.{foo: bar, baz:}', - error: 'Invalid token (Rbrace): "}"', + error: + 'Invalid jmespath expression: parse error at column 17, found unexpected token "}" (rbrace) for expression: a.{foo: bar, baz:}', }, { comment: 'Trailing comma', expression: 'a.{foo: bar, baz: bam, }', - error: 'Expecting an identifier token, got: Rbrace', + error: + 'Invalid jmespath expression: parse error at column 23, found unexpected token "}" (rbrace) for expression: a.{foo: bar, baz: bam, }', }, ])('multi-select hash errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in multi-select hash errors tests // Prepare const data = { type: 'object', @@ -578,30 +611,35 @@ describe('Syntax tests', () => { it.each([ { expression: 'foo ||', - error: 'Invalid token (EOF): ""', + error: + 'Invalid jmespath expression: parse error at column 6, found unexpected end of expression (EOF) for expression: foo ||', }, { expression: 'foo.|| bar', - error: 'Syntax error, unexpected token: ||(Or)', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "||" (or) for expression: foo.|| bar', }, { expression: ' || foo', - error: 'Invalid token (Or): "||"', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected token "||" (or) for expression: || foo', }, { expression: 'foo || || foo', - error: 'Invalid token (Or): "||"', + error: + 'Invalid jmespath expression: parse error at column 7, found unexpected token "||" (or) for expression: foo || || foo', }, { expression: 'foo.[a ||]', - error: 'Invalid token (Rbracket): "]"', + error: + 'Invalid jmespath expression: parse error at column 9, found unexpected token "]" (rbracket) for expression: foo.[a ||]', }, { expression: '"foo', - error: 'Unexpected end of JSON input', + error: + 'Bad jmespath expression: unknown token ""foo" at column 0 for expression: "foo', }, ])('boolean OR errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in boolean OR errors tests // Prepare const data = { type: 'object', @@ -663,93 +701,112 @@ describe('Syntax tests', () => { it.each([ { expression: 'foo[ ?bar==`"baz"`]', - error: 'Unknown character: ?', + error: + 'Bad jmespath expression: unknown token "?" at column 5 for expression: foo[ ?bar==`"baz"`]', }, { expression: 'foo[?bar==]', - error: 'Invalid token (Rbracket): "]"', + error: + 'Invalid jmespath expression: parse error at column 10, found unexpected token "]" (rbracket) for expression: foo[?bar==]', }, { expression: 'foo[?==]', - error: 'Invalid token (EQ): "=="', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected token "==" (eq) for expression: foo[?==]', }, { expression: 'foo[?==bar]', - error: 'Invalid token (EQ): "=="', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected token "==" (eq) for expression: foo[?==bar]', }, { expression: 'foo[?bar==baz?]', - error: 'Unknown character: ?', + error: + 'Bad jmespath expression: unknown token "?" at column 13 for expression: foo[?bar==baz?]', }, { comment: 'Literal char not escaped', expression: 'foo[?bar==`["foo`bar"]`]', - error: 'Unexpected end of JSON input', + error: + 'Bad jmespath expression: unknown token "["foo" at column 10 for expression: foo[?bar==`["foo`bar"]`]', }, { comment: 'Unknown comparator', expression: 'foo[?bar<>baz]', - error: 'Invalid token (GT): ">"', + error: + 'Invalid jmespath expression: parse error at column 9, found unexpected token ">" (gt) for expression: foo[?bar<>baz]', }, { comment: 'Unknown comparator', expression: 'foo[?bar^baz]', - error: 'Unknown character: ^', + error: + 'Bad jmespath expression: unknown token "^" at column 8 for expression: foo[?bar^baz]', }, { expression: 'foo[bar==baz]', - error: 'Expected Star, got: UnquotedIdentifier', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "bar" (unquoted_identifier) for expression: foo[bar==baz]', }, { expression: 'bar.`"anything"`', - error: 'Syntax error, unexpected token: anything(Literal)', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "anything" (literal) for expression: bar.`"anything"`', }, { expression: 'bar.baz.noexists.`"literal"`', - error: 'Syntax error, unexpected token: literal(Literal)', + error: + 'Invalid jmespath expression: parse error at column 17, found unexpected token "literal" (literal) for expression: bar.baz.noexists.`"literal"`', }, { comment: 'Literal wildcard projection', expression: 'foo[*].`"literal"`', - error: 'Syntax error, unexpected token: literal(Literal)', + error: + 'Invalid jmespath expression: parse error at column 7, found unexpected token "literal" (literal) for expression: foo[*].`"literal"`', }, { expression: 'foo[*].name.`"literal"`', - error: 'Syntax error, unexpected token: literal(Literal)', + error: + 'Invalid jmespath expression: parse error at column 12, found unexpected token "literal" (literal) for expression: foo[*].name.`"literal"`', }, { expression: 'foo[].name.`"literal"`', - error: 'Syntax error, unexpected token: literal(Literal)', + error: + 'Invalid jmespath expression: parse error at column 11, found unexpected token "literal" (literal) for expression: foo[].name.`"literal"`', }, { expression: 'foo[].name.`"literal"`.`"subliteral"`', - error: 'Syntax error, unexpected token: literal(Literal)', + error: + 'Invalid jmespath expression: parse error at column 11, found unexpected token "literal" (literal) for expression: foo[].name.`"literal"`.`"subliteral"`', }, { comment: 'Projecting a literal onto an empty list', expression: 'foo[*].name.noexist.`"literal"`', - error: 'Syntax error, unexpected token: literal(Literal)', + error: + 'Invalid jmespath expression: parse error at column 20, found unexpected token "literal" (literal) for expression: foo[*].name.noexist.`"literal"`', }, { expression: 'foo[].name.noexist.`"literal"`', - error: 'Syntax error, unexpected token: literal(Literal)', + error: + 'Invalid jmespath expression: parse error at column 19, found unexpected token "literal" (literal) for expression: foo[].name.noexist.`"literal"`', }, { expression: 'twolen[*].`"foo"`', - error: 'Syntax error, unexpected token: foo(Literal)', + error: + 'Invalid jmespath expression: parse error at column 10, found unexpected token "foo" (literal) for expression: twolen[*].`"foo"`', }, { comment: 'Two level projection of a literal', expression: 'twolen[*].threelen[*].`"bar"`', - error: 'Syntax error, unexpected token: bar(Literal)', + error: + 'Invalid jmespath expression: parse error at column 22, found unexpected token "bar" (literal) for expression: twolen[*].threelen[*].`"bar"`', }, { comment: 'Two level flattened projection of a literal', expression: 'twolen[].threelen[].`"bar"`', - error: 'Syntax error, unexpected token: bar(Literal)', + error: + 'Invalid jmespath expression: parse error at column 20, found unexpected token "bar" (literal) for expression: twolen[].threelen[].`"bar"`', }, ])('filter errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in filter errors tests // Prepare const data = { type: 'object', @@ -798,7 +855,7 @@ describe('Syntax tests', () => { }, ])('should support combined syntax', ({ expression, expected }) => { // Prepare - const data = { type: 'object' }; + const data: string[] = []; // Act const result = search(expression, data); From c79d9004d5716dd9c45f1dee5b8e5edc125730a2 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 7 Jun 2023 16:36:58 +0000 Subject: [PATCH 053/103] feat: error handling --- packages/jmespath/src/ParsedResult.ts | 20 +- packages/jmespath/src/Parser.ts | 4 +- packages/jmespath/src/errors.ts | 144 +++++++++++---- packages/jmespath/src/functions/Functions.ts | 5 +- .../jmespath/src/functions/typeChecking.ts | 29 +-- .../jmespath/src/visitor/TreeInterpreter.ts | 43 +++-- .../jmespath/tests/unit/functions.test.ts | 10 +- packages/jmespath/tests/unit/pipe.test.ts | 2 +- packages/jmespath/tests/unit/syntax.test.ts | 174 +++++++++--------- packages/jmespath/tests/unit/unicode.test.ts | 2 +- 10 files changed, 265 insertions(+), 168 deletions(-) diff --git a/packages/jmespath/src/ParsedResult.ts b/packages/jmespath/src/ParsedResult.ts index 998544eef8..0e5dc3b2ac 100644 --- a/packages/jmespath/src/ParsedResult.ts +++ b/packages/jmespath/src/ParsedResult.ts @@ -1,4 +1,10 @@ import { TreeInterpreter } from './visitor'; +import { + JMESPathTypeError, + UnknownFunctionError, + ArityError, + VariadicArityError, +} from './errors'; import type { Node, JSONValue, ParsingOptions } from './types'; class ParsedResult { @@ -13,7 +19,19 @@ class ParsedResult { public search(value: JSONValue, options?: ParsingOptions): unknown { const interpreter = new TreeInterpreter(options); - return interpreter.visit(this.parsed, value); + try { + return interpreter.visit(this.parsed, value); + } catch (error) { + if ( + error instanceof JMESPathTypeError || + error instanceof UnknownFunctionError || + error instanceof ArityError || + error instanceof VariadicArityError + ) { + error.setExpression(this.expression); + } + throw error; + } } } diff --git a/packages/jmespath/src/Parser.ts b/packages/jmespath/src/Parser.ts index 67e8e4090c..aa33a9df98 100644 --- a/packages/jmespath/src/Parser.ts +++ b/packages/jmespath/src/Parser.ts @@ -111,9 +111,9 @@ class Parser { ) { error.setExpression(expression); throw error; - } else { - throw error; } + + throw error; } } diff --git a/packages/jmespath/src/errors.ts b/packages/jmespath/src/errors.ts index ec471a91e0..cec250867a 100644 --- a/packages/jmespath/src/errors.ts +++ b/packages/jmespath/src/errors.ts @@ -29,7 +29,7 @@ class JMESPathError extends Error { this.expression = expression; // Set the message to include the expression. - this.message = `${this.message} for expression: ${this.expression}`; + this.message = `${this.message} in expression: ${this.expression}`; } } @@ -119,29 +119,87 @@ class IncompleteExpressionError extends ParseError { } /** - * TODO: write docs for ArityError + * TODO: write docs for EmptyExpressionError + * TODO: see if this is ever being thrown + */ +class EmptyExpressionError extends JMESPathError { + public constructor() { + super('Invalid JMESPath expression: cannot be empty.'); + this.name = 'EmptyExpressionError'; + } +} + +/** + * Base class for errors thrown during function execution. + * + * When writing a JMESPath expression, you can use functions to transform the + * data. For example, the `abs()` function returns the absolute value of a number. + * + * If an error occurs during function execution, the error is thrown as a + * subclass of `FunctionError`. The subclass is determined by the type of error + * that occurred. + * + * Errors can be thrown while validating the arguments passed to a function, or + * while executing the function itself. */ -class ArityError extends JMESPathError { +class FunctionError extends JMESPathError { + /** + * Function that was being executed when the error occurred. + * Can be set by whatever catches the error. + */ + public functionName?: string; + + public constructor(message: string) { + super(message); + this.name = 'FunctionError'; + } + + /** + * Set the function that was being validated or executed when the error occurred. + * + * The separate method allows the name to be set after the error is + * thrown. In most cases the error is thrown down the call stack, but we want + * to show the actual function name used in the expression rather than an internal + * alias. To avoid passing the function name down the call stack, we set it + * after the error is thrown. + * + * @param functionName The function that was being validated or executed when the error occurred. + */ + public setFunctionName(functionName: string): void { + this.message = this.message.replace( + 'for function undefined', + `for function ${functionName}()` + ); + } +} + +/** + * Error thrown when an unexpected argument is passed to a function. + * + * Function arguments are validated before the function is executed. If an + * invalid argument is passed, the error is thrown. For example, the `abs()` + * function expects exactly one argument. If more than one argument is passed, + * an `ArityError` is thrown. + */ +class ArityError extends FunctionError { public actualArity: number; public expectedArity: number; - public functionName: string; - public constructor(options: { - expectedArity: number; - actualArity: number; - functionName: string; - }) { + public constructor(options: { expectedArity: number; actualArity: number }) { super('Invalid arity for JMESPath function'); this.name = 'ArityError'; this.actualArity = options.actualArity; this.expectedArity = options.expectedArity; - this.functionName = options.functionName; + + const arityParticle = + this.actualArity > this.expectedArity ? 'at most' : 'at least'; // Set the message to include the error info. - this.message = `Expected at least ${this.expectedArity} ${this.pluralize( - 'argument', + this.message = `Expected ${arityParticle} ${ this.expectedArity - )} for function ${this.functionName}, received: ${this.actualArity}`; + } ${this.pluralize('argument', this.expectedArity)} for function ${ + this.functionName + }, received ${this.actualArity}`; } protected pluralize(word: string, count: number): string { @@ -153,60 +211,74 @@ class ArityError extends JMESPathError { * TODO: write docs for VariadicArityError */ class VariadicArityError extends ArityError { - public constructor(options: { - expectedArity: number; - actualArity: number; - functionName: string; - }) { + public constructor(options: { expectedArity: number; actualArity: number }) { super(options); this.name = 'VariadicArityError'; + + // Set the message to include the error info. + this.message = `Expected ${this.expectedArity} ${this.pluralize( + 'argument', + this.expectedArity + )} for function ${this.functionName}, received ${this.actualArity}`; } } /** - * TODO: write docs for JMESPathTypeError + * Error thrown when an invalid argument type is passed to a built-in function. + * + * Function arguments are validated before the function is executed. If an + * invalid argument type is found, this error is thrown. For example, the + * `abs()` function expects a number as its argument. If a string is passed + * instead, this error is thrown. */ -class JMESPathTypeError extends JMESPathError { +class JMESPathTypeError extends FunctionError { public actualType: string; public currentValue: unknown; - public expectedTypes: string; - public functionName: string; + public expectedTypes: string[]; public constructor(options: { - functionName: string; currentValue: unknown; actualType: string; - expectedTypes: string; + expectedTypes: string[]; }) { super('Invalid type for JMESPath expression'); this.name = 'JMESPathTypeError'; - this.functionName = options.functionName; this.currentValue = options.currentValue; this.actualType = options.actualType; this.expectedTypes = options.expectedTypes; // Set the message to include the error info. - this.message = `${super.message}: function ${ + this.message = `Invalid argument type for function ${ this.functionName - } expected one of: ${this.expectedTypes}, received: ${this.actualType}`; + }, expected ${this.serializeExpectedTypes()} but found "${ + this.actualType + }"`; + } + + protected serializeExpectedTypes(): string { + return this.expectedTypes.length === 1 + ? `"${this.expectedTypes[0]}"` + : `one of ${this.expectedTypes.map((type) => `"${type}"`).join(', ')}`; } } /** - * TODO: write docs for EmptyExpressionError + * Error thrown when an unknown function is used in an expression. + * + * When evaluating a JMESPath expression, the interpreter looks up the function + * name in a table of built-in functions, as well as any custom functions + * provided by the user. If the function name is not found, this error is thrown. */ -class EmptyExpressionError extends JMESPathError { +class UnknownFunctionError extends FunctionError { public constructor() { - super('Invalid JMESPath expression: cannot be empty.'); - this.name = 'EmptyExpressionError'; + super('Unknown function'); + this.name = 'UnknownFunctionError'; + + // Set the message to include the error info. + this.message = `Unknown function:`; } } -/** - * TODO: write docs for UnknownFunctionError - */ -class UnknownFunctionError extends JMESPathError {} - export { JMESPathError, LexerError, diff --git a/packages/jmespath/src/functions/Functions.ts b/packages/jmespath/src/functions/Functions.ts index 3de932e6a4..cfe2c98eb7 100644 --- a/packages/jmespath/src/functions/Functions.ts +++ b/packages/jmespath/src/functions/Functions.ts @@ -215,14 +215,13 @@ class Functions { if (typeof originalMethod !== 'function') { throw new TypeError('Only methods can be decorated with @signature.'); } - const methodName = originalMethod.name; // Use a function() {} instead of an () => {} arrow function so that we can // access `myClass` as `this` in a decorated `myClass.myMethod()`. descriptor.value = function (args: unknown[]) { const { variadic, argumentsSpecs } = options; - arityCheck(args, argumentsSpecs, methodName, variadic); - typeCheck(args, argumentsSpecs, methodName); + arityCheck(args, argumentsSpecs, variadic); + typeCheck(args, argumentsSpecs); return originalMethod.apply(this, args); }; diff --git a/packages/jmespath/src/functions/typeChecking.ts b/packages/jmespath/src/functions/typeChecking.ts index 2fd84eaac2..c79302bb50 100644 --- a/packages/jmespath/src/functions/typeChecking.ts +++ b/packages/jmespath/src/functions/typeChecking.ts @@ -11,20 +11,17 @@ import { JMESPathTypeError, ArityError, VariadicArityError } from '../errors'; const arityCheck = ( args: unknown[], argumentsSpecs: Array>, - decoratedFuncName: string, variadic?: boolean ): void => { if (variadic) { if (args.length < argumentsSpecs.length) { throw new VariadicArityError({ - functionName: decoratedFuncName, expectedArity: argumentsSpecs.length, actualArity: args.length, }); } } else if (args.length !== argumentsSpecs.length) { throw new ArityError({ - functionName: decoratedFuncName, expectedArity: argumentsSpecs.length, actualArity: args.length, }); @@ -35,15 +32,13 @@ const arityCheck = ( * TODO: write docs for typeCheck() * @param args * @param argumentsSpecs - * @param name */ const typeCheck = ( args: unknown[], - argumentsSpecs: Array>, - decoratedFuncName: string + argumentsSpecs: Array> ): void => { argumentsSpecs.forEach((argumentSpec, index) => { - typeCheckArgument(args[index], argumentSpec, decoratedFuncName); + typeCheckArgument(args[index], argumentSpec); }); }; @@ -56,13 +51,8 @@ const typeCheck = ( * * @param arg * @param argumentSpec - * @param decoratedFuncName */ -const typeCheckArgument = ( - arg: unknown, - argumentSpec: Array, - decoratedFuncName: string -): void => { +const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { // TODO: check if all types in argumentSpec are valid if (argumentSpec.length === 0 || argumentSpec[0] === 'any') { return; @@ -71,34 +61,31 @@ const typeCheckArgument = ( if (type.startsWith('array')) { if (!Array.isArray(arg)) { throw new JMESPathTypeError({ - functionName: decoratedFuncName, currentValue: arg, - expectedTypes: argumentSpec.join(', '), + expectedTypes: argumentSpec, actualType: typeof arg, }); } if (type.includes('-')) { const arrayItemsType = type.slice(6); arg.forEach((element) => { - typeCheckArgument(element, [arrayItemsType], decoratedFuncName); + typeCheckArgument(element, [arrayItemsType]); }); } } else { if (type === 'string' || type === 'number' || type === 'boolean') { if (typeof arg !== type) { throw new JMESPathTypeError({ - functionName: decoratedFuncName, currentValue: arg, - expectedTypes: argumentSpec.join(', '), - actualType: typeof arg, + expectedTypes: argumentSpec, + actualType: type === 'boolean' ? 'boolean' : typeof arg, // TODO: fix this }); } } else if (type === 'null') { if (!Object.is(arg, null)) { throw new JMESPathTypeError({ - functionName: decoratedFuncName, currentValue: arg, - expectedTypes: argumentSpec.join(', '), + expectedTypes: argumentSpec, actualType: typeof arg, }); } diff --git a/packages/jmespath/src/visitor/TreeInterpreter.ts b/packages/jmespath/src/visitor/TreeInterpreter.ts index 319064bb25..a2da3cd8b1 100644 --- a/packages/jmespath/src/visitor/TreeInterpreter.ts +++ b/packages/jmespath/src/visitor/TreeInterpreter.ts @@ -1,6 +1,12 @@ import type { JSONValue, Node, TreeInterpreterOptions } from '../types'; import { Functions } from '../functions'; import { Expression, isRecord, isTruthy } from './utils'; +import { + ArityError, + JMESPathTypeError, + UnknownFunctionError, + VariadicArityError, +} from '../errors'; class TreeInterpreter { #functions: Functions; @@ -195,21 +201,34 @@ class TreeInterpreter { const normalizedFunctionName = node.value.replace(/_([a-z])/g, (g) => g[1].toUpperCase() ); - const methodName = methods.find( - (method) => method === `func${normalizedFunctionName}` - ); + // capitalize first letter & add `func` prefix + const funcName = `func${ + normalizedFunctionName.charAt(0).toUpperCase() + + normalizedFunctionName.slice(1) + }`; + const methodName = methods.find((method) => method === funcName); if (!methodName) { - // TODO: convert to a custom error - throw new Error(`Function not found: ${node.value}`); + throw new UnknownFunctionError(); } - // We know that methodName is a key of this.#functions, but TypeScript - // doesn't know that, so we have to use @ts-ignore to tell it that it's - // okay. We could use a type assertion like `as keyof Functions`, but - // we also want to keep the args generic, so for now we'll just ignore it. - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-next-line - return this.#functions[methodName](args); + try { + // We know that methodName is a key of this.#functions, but TypeScript + // doesn't know that, so we have to use @ts-ignore to tell it that it's + // okay. We could use a type assertion like `as keyof Functions`, but + // we also want to keep the args generic, so for now we'll just ignore it. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-next-line + return this.#functions[methodName](args); + } catch (error) { + if ( + error instanceof JMESPathTypeError || + error instanceof ArityError || + error instanceof VariadicArityError + ) { + error.setFunctionName(node.value); + throw error; + } + } } /** diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/functions.test.ts index 166201a1cf..2dde2318b6 100644 --- a/packages/jmespath/tests/unit/functions.test.ts +++ b/packages/jmespath/tests/unit/functions.test.ts @@ -62,20 +62,22 @@ describe('Functions tests', () => { { expression: 'abs(str)', error: - 'TypeError: abs() expected argument 1 to be type (number) but received type string instead.', + 'Invalid argument type for function abs(), expected "number" but found "string" in expression: abs(str)', }, { expression: 'abs(`false`)', error: - 'TypeError: abs() expected argument 1 to be type (number) but received type boolean instead.', + 'Invalid argument type for function abs(), expected "number" but found "boolean" in expression: abs(`false`)', }, { expression: 'abs(`1`, `2`)', - error: 'ArgumentError: abs() takes 1 argument but received 2', + error: + 'Expected at most 1 argument for function abs(), received 2 in expression: abs(`1`, `2`)', }, { expression: 'abs()', - error: 'ArgumentError: abs() takes 1 argument but received 0', + error: + 'Expected at least 1 argument for function abs(), received 0 in expression: abs()', }, ])('abs() function errors', ({ expression, error }) => { // TODO: see if we can assert the error type as well in abs() fn errors tests diff --git a/packages/jmespath/tests/unit/pipe.test.ts b/packages/jmespath/tests/unit/pipe.test.ts index b311eb3352..e9a51f8f8f 100644 --- a/packages/jmespath/tests/unit/pipe.test.ts +++ b/packages/jmespath/tests/unit/pipe.test.ts @@ -146,7 +146,7 @@ describe('Pipe expressions tests', () => { }, { expression: '`null`|[@]', - expected: [null], + expected: null, }, ])( 'should support piping with wildcard and current operators', diff --git a/packages/jmespath/tests/unit/syntax.test.ts b/packages/jmespath/tests/unit/syntax.test.ts index 15c1c4c975..82bcf96e86 100644 --- a/packages/jmespath/tests/unit/syntax.test.ts +++ b/packages/jmespath/tests/unit/syntax.test.ts @@ -32,37 +32,37 @@ describe('Syntax tests', () => { { expression: 'foo.1', error: - 'Invalid jmespath expression: parse error at column 4, found unexpected token "1" (number) for expression: foo.1', + 'Invalid jmespath expression: parse error at column 4, found unexpected token "1" (number) in expression: foo.1', }, { expression: 'foo.-11', error: - 'Invalid jmespath expression: parse error at column 4, found unexpected token "-11" (number) for expression: foo.-11', + 'Invalid jmespath expression: parse error at column 4, found unexpected token "-11" (number) in expression: foo.-11', }, { expression: 'foo.', error: - 'Invalid jmespath expression: parse error at column 4, found unexpected end of expression (EOF) for expression: foo.', + 'Invalid jmespath expression: parse error at column 4, found unexpected end of expression (EOF) in expression: foo.', }, { expression: '.foo', error: - 'Invalid jmespath expression: parse error at column 0, found unexpected token "." (dot) for expression: .foo', + 'Invalid jmespath expression: parse error at column 0, found unexpected token "." (dot) in expression: .foo', }, { expression: 'foo..bar', error: - 'Invalid jmespath expression: parse error at column 4, found unexpected token "." (dot) for expression: foo..bar', + 'Invalid jmespath expression: parse error at column 4, found unexpected token "." (dot) in expression: foo..bar', }, { expression: 'foo.bar.', error: - 'Invalid jmespath expression: parse error at column 8, found unexpected end of expression (EOF) for expression: foo.', + 'Invalid jmespath expression: parse error at column 8, found unexpected end of expression (EOF) in expression: foo.', }, { expression: 'foo[.]', error: - 'Invalid jmespath expression: parse error at column 4, found unexpected token "." (dot) for expression: foo[.]', + 'Invalid jmespath expression: parse error at column 4, found unexpected token "." (dot) in expression: foo[.]', }, ])('dot syntax errors', ({ expression, error }) => { // Prepare @@ -78,72 +78,72 @@ describe('Syntax tests', () => { { expression: '.', error: - 'Invalid jmespath expression: parse error at column 0, found unexpected token "." (dot) for expression: .', + 'Invalid jmespath expression: parse error at column 0, found unexpected token "." (dot) in expression: .', }, { expression: ':', error: - 'Invalid jmespath expression: parse error at column 0, found unexpected token ":" (colon) for expression: :', + 'Invalid jmespath expression: parse error at column 0, found unexpected token ":" (colon) in expression: :', }, { expression: ',', error: - 'Invalid jmespath expression: parse error at column 0, found unexpected token "," (comma) for expression: ,', + 'Invalid jmespath expression: parse error at column 0, found unexpected token "," (comma) in expression: ,', }, { expression: ']', error: - 'Invalid jmespath expression: parse error at column 0, found unexpected token "]" (rbracket) for expression: ]', + 'Invalid jmespath expression: parse error at column 0, found unexpected token "]" (rbracket) in expression: ]', }, { expression: '[', error: - 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) for expression: [', + 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) in expression: [', }, { expression: '}', error: - 'Invalid jmespath expression: parse error at column 0, found unexpected token "}" (rbrace) for expression: }', + 'Invalid jmespath expression: parse error at column 0, found unexpected token "}" (rbrace) in expression: }', }, { expression: '{', error: - 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) for expression: {', + 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) in expression: {', }, { expression: ')', error: - 'Invalid jmespath expression: parse error at column 0, found unexpected token ")" (rparen) for expression: )', + 'Invalid jmespath expression: parse error at column 0, found unexpected token ")" (rparen) in expression: )', }, { expression: '(', error: - 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) for expression: (', + 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) in expression: (', }, { expression: '((&', error: - 'Invalid jmespath expression: parse error at column 3, found unexpected end of expression (EOF) for expression: ((&', + 'Invalid jmespath expression: parse error at column 3, found unexpected end of expression (EOF) in expression: ((&', }, { expression: 'a[', error: - 'Invalid jmespath expression: parse error at column 2, found unexpected end of expression (EOF) for expression: a[', + 'Invalid jmespath expression: parse error at column 2, found unexpected end of expression (EOF) in expression: a[', }, { expression: 'a]', error: - 'Invalid jmespath expression: parse error at column 1, found unexpected token "]" (rbracket) for expression: a]', + 'Invalid jmespath expression: parse error at column 1, found unexpected token "]" (rbracket) in expression: a]', }, { expression: 'a][', error: - 'Invalid jmespath expression: parse error at column 1, found unexpected token "]" (rbracket) for expression: a]', + 'Invalid jmespath expression: parse error at column 1, found unexpected token "]" (rbracket) in expression: a]', }, { expression: '!', error: - 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) for expression: !', + 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) in expression: !', }, ])('simple token errors', ({ expression, error }) => { // Prepare @@ -159,7 +159,7 @@ describe('Syntax tests', () => { { expression: '![!(!', error: - 'Invalid jmespath expression: parse error at column 5, found unexpected end of expression (EOF) for expression: ![!(!', + 'Invalid jmespath expression: parse error at column 5, found unexpected end of expression (EOF) in expression: ![!(!', }, ])('boolean token errors', ({ expression, error }) => { // Prepare @@ -205,27 +205,27 @@ describe('Syntax tests', () => { { expression: '.*', error: - 'Invalid jmespath expression: parse error at column 0, found unexpected token "." (dot) for expression: .*', + 'Invalid jmespath expression: parse error at column 0, found unexpected token "." (dot) in expression: .*', }, { expression: '*foo', error: - 'Invalid jmespath expression: parse error at column 1, found unexpected token "foo" (unquoted_identifier) for expression: *foo', + 'Invalid jmespath expression: parse error at column 1, found unexpected token "foo" (unquoted_identifier) in expression: *foo', }, { expression: '*0', error: - 'Invalid jmespath expression: parse error at column 1, found unexpected token "0" (number) for expression: *0', + 'Invalid jmespath expression: parse error at column 1, found unexpected token "0" (number) in expression: *0', }, { expression: 'foo[*]bar', error: - 'Invalid jmespath expression: parse error at column 6, found unexpected token "bar" (unquoted_identifier) for expression: foo[*]bar', + 'Invalid jmespath expression: parse error at column 6, found unexpected token "bar" (unquoted_identifier) in expression: foo[*]bar', }, { expression: 'foo[*]*', error: - 'Invalid jmespath expression: parse error at column 6, found unexpected token "*" (star) for expression: foo[*]*', + 'Invalid jmespath expression: parse error at column 6, found unexpected token "*" (star) in expression: foo[*]*', }, ])('wildcard token errors', ({ expression, error }) => { // Prepare @@ -293,12 +293,12 @@ describe('Syntax tests', () => { { expression: '*.[0]', error: - 'Invalid jmespath expression: parse error at column 3, found unexpected token "0" (number) for expression: *.[0]', + 'Invalid jmespath expression: parse error at column 3, found unexpected token "0" (number) in expression: *.[0]', }, { expression: 'foo[#]', error: - 'Bad jmespath expression: unknown token "#" at column 4 for expression: foo[#]', + 'Bad jmespath expression: unknown token "#" at column 4 in expression: foo[#]', }, ])('simple breacket errors', ({ expression, error }) => { // Prepare @@ -347,85 +347,85 @@ describe('Syntax tests', () => { comment: 'Valid multi-select of a list', expression: 'foo[0, 1]', error: - 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) for expression: foo[0, 1]', + 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) in expression: foo[0, 1]', }, { expression: 'foo.[0]', error: - 'Invalid jmespath expression: parse error at column 5, found unexpected token "0" (number) for expression: foo.[0]', + 'Invalid jmespath expression: parse error at column 5, found unexpected token "0" (number) in expression: foo.[0]', }, { comment: 'Multi-select of a list with trailing comma', expression: 'foo[0, ]', error: - 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) for expression: foo[0, ]', + 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) in expression: foo[0, ]', }, { comment: 'Multi-select of a list with trailing comma and no close', expression: 'foo[0,', error: - 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) for expression: foo[0,', + 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) in expression: foo[0,', }, { comment: 'Multi-select of a list with trailing comma and no close', expression: 'foo.[a', error: - 'Invalid jmespath expression: parse error at column 6, found unexpected end of expression (EOF) for expression: foo.[a', + 'Invalid jmespath expression: parse error at column 6, found unexpected end of expression (EOF) in expression: foo.[a', }, { comment: 'Multi-select of a list with extra comma', expression: 'foo[0,, 1]', error: - 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) for expression: foo[0,, 1]', + 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) in expression: foo[0,, 1]', }, { comment: 'Multi-select of a list using an identifier index', expression: 'foo[abc]', error: - 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) for expression: foo[abc]', + 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) in expression: foo[abc]', }, { comment: 'Multi-select of a list using identifier indices', expression: 'foo[abc, def]', error: - 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) for expression: foo[abc, def]', + 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) in expression: foo[abc, def]', }, { comment: 'Multi-select of a list using an identifier index', expression: 'foo[abc, 1]', error: - 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) for expression: foo[abc, 1]', + 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) in expression: foo[abc, 1]', }, { comment: 'Multi-select of a list using an identifier index with trailing comma', expression: 'foo[abc, ]', error: - 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) for expression: foo[abc, ]', + 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) in expression: foo[abc, ]', }, { comment: 'Multi-select of a hash using a numeric index', expression: 'foo.[abc, 1]', error: - 'Invalid jmespath expression: parse error at column 10, found unexpected token "1" (number) for expression: foo.[abc, 1]', + 'Invalid jmespath expression: parse error at column 10, found unexpected token "1" (number) in expression: foo.[abc, 1]', }, { comment: 'Multi-select of a hash with a trailing comma', expression: 'foo.[abc, ]', error: - 'Invalid jmespath expression: parse error at column 10, found unexpected token "]" (rbracket) for expression: foo.[abc, ]', + 'Invalid jmespath expression: parse error at column 10, found unexpected token "]" (rbracket) in expression: foo.[abc, ]', }, { comment: 'Multi-select of a hash with extra commas', expression: 'foo.[abc,, def]', error: - 'Invalid jmespath expression: parse error at column 9, found unexpected token "," (comma) for expression: foo.[abc,, def]', + 'Invalid jmespath expression: parse error at column 9, found unexpected token "," (comma) in expression: foo.[abc,, def]', }, { comment: 'Multi-select of a hash using number indices', expression: 'foo.[0, 1]', error: - 'Invalid jmespath expression: parse error at column 5, found unexpected token "0" (number) for expression: foo.[0, 1]', + 'Invalid jmespath expression: parse error at column 5, found unexpected token "0" (number) in expression: foo.[0, 1]', }, ])('multi-select list errors', ({ expression, error }) => { // Prepare @@ -473,108 +473,108 @@ describe('Syntax tests', () => { comment: 'No key or value', expression: 'a{}', error: - 'Invalid jmespath expression: parse error at column 2, found unexpected token "}" (rbrace) for expression: a{}', + 'Invalid jmespath expression: parse error at column 2, found unexpected token "}" (rbrace) in expression: a{}', }, { comment: 'No closing token', expression: 'a{', error: - 'Invalid jmespath expression: parse error at column 2, found unexpected end of expression (EOF) for expression: a{', + 'Invalid jmespath expression: parse error at column 2, found unexpected end of expression (EOF) in expression: a{', }, { comment: 'Not a key value pair', expression: 'a{foo}', error: - 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo}', + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo}', }, { comment: 'Missing value and closing character', expression: 'a{foo:', error: - 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo:', + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo:', }, { comment: 'Missing closing character', expression: 'a{foo: 0', error: - 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo: 0', + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo: 0', }, { comment: 'Missing value', expression: 'a{foo:}', error: - 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo:}', + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo:}', }, { comment: 'Trailing comma and no closing character', expression: 'a{foo: 0, ', error: - 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo: 0, ', + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo: 0, ', }, { comment: 'Missing value with trailing comma', expression: 'a{foo: ,}', error: - 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo: ,}', + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo: ,}', }, { comment: 'Accessing Array using an identifier', expression: 'a{foo: bar}', error: - 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo: bar}', + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo: bar}', }, { expression: 'a{foo: 0}', error: - 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) for expression: a{foo: 0}', + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo: 0}', }, { comment: 'Missing key-value pair', expression: 'a.{}', error: - 'Invalid jmespath expression: parse error at column 3, found unexpected token "}" (rbrace) for expression: a.{}', + 'Invalid jmespath expression: parse error at column 3, found unexpected token "}" (rbrace) in expression: a.{}', }, { comment: 'Not a key-value pair', expression: 'a.{foo}', error: - 'Invalid jmespath expression: parse error at column 6, found unexpected token "}" (rbrace) for expression: a.{foo}', + 'Invalid jmespath expression: parse error at column 6, found unexpected token "}" (rbrace) in expression: a.{foo}', }, { comment: 'Missing value', expression: 'a.{foo:}', error: - 'Invalid jmespath expression: parse error at column 7, found unexpected token "}" (rbrace) for expression: a.{foo:}', + 'Invalid jmespath expression: parse error at column 7, found unexpected token "}" (rbrace) in expression: a.{foo:}', }, { comment: 'Missing value with trailing comma', expression: 'a.{foo: ,}', error: - 'Invalid jmespath expression: parse error at column 8, found unexpected token "," (comma) for expression: a.{foo: ,}', + 'Invalid jmespath expression: parse error at column 8, found unexpected token "," (comma) in expression: a.{foo: ,}', }, { comment: 'Trailing comma', expression: 'a.{foo: bar, }', error: - 'Invalid jmespath expression: parse error at column 13, found unexpected token "}" (rbrace) for expression: a.{foo: bar, }', + 'Invalid jmespath expression: parse error at column 13, found unexpected token "}" (rbrace) in expression: a.{foo: bar, }', }, { comment: 'Missing key in second key-value pair', expression: 'a.{foo: bar, baz}', error: - 'Invalid jmespath expression: parse error at column 16, found unexpected token "}" (rbrace) for expression: a.{foo: bar, baz}', + 'Invalid jmespath expression: parse error at column 16, found unexpected token "}" (rbrace) in expression: a.{foo: bar, baz}', }, { comment: 'Missing value in second key-value pair', expression: 'a.{foo: bar, baz:}', error: - 'Invalid jmespath expression: parse error at column 17, found unexpected token "}" (rbrace) for expression: a.{foo: bar, baz:}', + 'Invalid jmespath expression: parse error at column 17, found unexpected token "}" (rbrace) in expression: a.{foo: bar, baz:}', }, { comment: 'Trailing comma', expression: 'a.{foo: bar, baz: bam, }', error: - 'Invalid jmespath expression: parse error at column 23, found unexpected token "}" (rbrace) for expression: a.{foo: bar, baz: bam, }', + 'Invalid jmespath expression: parse error at column 23, found unexpected token "}" (rbrace) in expression: a.{foo: bar, baz: bam, }', }, ])('multi-select hash errors', ({ expression, error }) => { // Prepare @@ -612,32 +612,32 @@ describe('Syntax tests', () => { { expression: 'foo ||', error: - 'Invalid jmespath expression: parse error at column 6, found unexpected end of expression (EOF) for expression: foo ||', + 'Invalid jmespath expression: parse error at column 6, found unexpected end of expression (EOF) in expression: foo ||', }, { expression: 'foo.|| bar', error: - 'Invalid jmespath expression: parse error at column 4, found unexpected token "||" (or) for expression: foo.|| bar', + 'Invalid jmespath expression: parse error at column 4, found unexpected token "||" (or) in expression: foo.|| bar', }, { expression: ' || foo', error: - 'Invalid jmespath expression: parse error at column 1, found unexpected token "||" (or) for expression: || foo', + 'Invalid jmespath expression: parse error at column 1, found unexpected token "||" (or) in expression: || foo', }, { expression: 'foo || || foo', error: - 'Invalid jmespath expression: parse error at column 7, found unexpected token "||" (or) for expression: foo || || foo', + 'Invalid jmespath expression: parse error at column 7, found unexpected token "||" (or) in expression: foo || || foo', }, { expression: 'foo.[a ||]', error: - 'Invalid jmespath expression: parse error at column 9, found unexpected token "]" (rbracket) for expression: foo.[a ||]', + 'Invalid jmespath expression: parse error at column 9, found unexpected token "]" (rbracket) in expression: foo.[a ||]', }, { expression: '"foo', error: - 'Bad jmespath expression: unknown token ""foo" at column 0 for expression: "foo', + 'Bad jmespath expression: unknown token ""foo" at column 0 in expression: "foo', }, ])('boolean OR errors', ({ expression, error }) => { // Prepare @@ -702,109 +702,109 @@ describe('Syntax tests', () => { { expression: 'foo[ ?bar==`"baz"`]', error: - 'Bad jmespath expression: unknown token "?" at column 5 for expression: foo[ ?bar==`"baz"`]', + 'Bad jmespath expression: unknown token "?" at column 5 in expression: foo[ ?bar==`"baz"`]', }, { expression: 'foo[?bar==]', error: - 'Invalid jmespath expression: parse error at column 10, found unexpected token "]" (rbracket) for expression: foo[?bar==]', + 'Invalid jmespath expression: parse error at column 10, found unexpected token "]" (rbracket) in expression: foo[?bar==]', }, { expression: 'foo[?==]', error: - 'Invalid jmespath expression: parse error at column 5, found unexpected token "==" (eq) for expression: foo[?==]', + 'Invalid jmespath expression: parse error at column 5, found unexpected token "==" (eq) in expression: foo[?==]', }, { expression: 'foo[?==bar]', error: - 'Invalid jmespath expression: parse error at column 5, found unexpected token "==" (eq) for expression: foo[?==bar]', + 'Invalid jmespath expression: parse error at column 5, found unexpected token "==" (eq) in expression: foo[?==bar]', }, { expression: 'foo[?bar==baz?]', error: - 'Bad jmespath expression: unknown token "?" at column 13 for expression: foo[?bar==baz?]', + 'Bad jmespath expression: unknown token "?" at column 13 in expression: foo[?bar==baz?]', }, { comment: 'Literal char not escaped', expression: 'foo[?bar==`["foo`bar"]`]', error: - 'Bad jmespath expression: unknown token "["foo" at column 10 for expression: foo[?bar==`["foo`bar"]`]', + 'Bad jmespath expression: unknown token "["foo" at column 10 in expression: foo[?bar==`["foo`bar"]`]', }, { comment: 'Unknown comparator', expression: 'foo[?bar<>baz]', error: - 'Invalid jmespath expression: parse error at column 9, found unexpected token ">" (gt) for expression: foo[?bar<>baz]', + 'Invalid jmespath expression: parse error at column 9, found unexpected token ">" (gt) in expression: foo[?bar<>baz]', }, { comment: 'Unknown comparator', expression: 'foo[?bar^baz]', error: - 'Bad jmespath expression: unknown token "^" at column 8 for expression: foo[?bar^baz]', + 'Bad jmespath expression: unknown token "^" at column 8 in expression: foo[?bar^baz]', }, { expression: 'foo[bar==baz]', error: - 'Invalid jmespath expression: parse error at column 4, found unexpected token "bar" (unquoted_identifier) for expression: foo[bar==baz]', + 'Invalid jmespath expression: parse error at column 4, found unexpected token "bar" (unquoted_identifier) in expression: foo[bar==baz]', }, { expression: 'bar.`"anything"`', error: - 'Invalid jmespath expression: parse error at column 4, found unexpected token "anything" (literal) for expression: bar.`"anything"`', + 'Invalid jmespath expression: parse error at column 4, found unexpected token "anything" (literal) in expression: bar.`"anything"`', }, { expression: 'bar.baz.noexists.`"literal"`', error: - 'Invalid jmespath expression: parse error at column 17, found unexpected token "literal" (literal) for expression: bar.baz.noexists.`"literal"`', + 'Invalid jmespath expression: parse error at column 17, found unexpected token "literal" (literal) in expression: bar.baz.noexists.`"literal"`', }, { comment: 'Literal wildcard projection', expression: 'foo[*].`"literal"`', error: - 'Invalid jmespath expression: parse error at column 7, found unexpected token "literal" (literal) for expression: foo[*].`"literal"`', + 'Invalid jmespath expression: parse error at column 7, found unexpected token "literal" (literal) in expression: foo[*].`"literal"`', }, { expression: 'foo[*].name.`"literal"`', error: - 'Invalid jmespath expression: parse error at column 12, found unexpected token "literal" (literal) for expression: foo[*].name.`"literal"`', + 'Invalid jmespath expression: parse error at column 12, found unexpected token "literal" (literal) in expression: foo[*].name.`"literal"`', }, { expression: 'foo[].name.`"literal"`', error: - 'Invalid jmespath expression: parse error at column 11, found unexpected token "literal" (literal) for expression: foo[].name.`"literal"`', + 'Invalid jmespath expression: parse error at column 11, found unexpected token "literal" (literal) in expression: foo[].name.`"literal"`', }, { expression: 'foo[].name.`"literal"`.`"subliteral"`', error: - 'Invalid jmespath expression: parse error at column 11, found unexpected token "literal" (literal) for expression: foo[].name.`"literal"`.`"subliteral"`', + 'Invalid jmespath expression: parse error at column 11, found unexpected token "literal" (literal) in expression: foo[].name.`"literal"`.`"subliteral"`', }, { comment: 'Projecting a literal onto an empty list', expression: 'foo[*].name.noexist.`"literal"`', error: - 'Invalid jmespath expression: parse error at column 20, found unexpected token "literal" (literal) for expression: foo[*].name.noexist.`"literal"`', + 'Invalid jmespath expression: parse error at column 20, found unexpected token "literal" (literal) in expression: foo[*].name.noexist.`"literal"`', }, { expression: 'foo[].name.noexist.`"literal"`', error: - 'Invalid jmespath expression: parse error at column 19, found unexpected token "literal" (literal) for expression: foo[].name.noexist.`"literal"`', + 'Invalid jmespath expression: parse error at column 19, found unexpected token "literal" (literal) in expression: foo[].name.noexist.`"literal"`', }, { expression: 'twolen[*].`"foo"`', error: - 'Invalid jmespath expression: parse error at column 10, found unexpected token "foo" (literal) for expression: twolen[*].`"foo"`', + 'Invalid jmespath expression: parse error at column 10, found unexpected token "foo" (literal) in expression: twolen[*].`"foo"`', }, { comment: 'Two level projection of a literal', expression: 'twolen[*].threelen[*].`"bar"`', error: - 'Invalid jmespath expression: parse error at column 22, found unexpected token "bar" (literal) for expression: twolen[*].threelen[*].`"bar"`', + 'Invalid jmespath expression: parse error at column 22, found unexpected token "bar" (literal) in expression: twolen[*].threelen[*].`"bar"`', }, { comment: 'Two level flattened projection of a literal', expression: 'twolen[].threelen[].`"bar"`', error: - 'Invalid jmespath expression: parse error at column 20, found unexpected token "bar" (literal) for expression: twolen[].threelen[].`"bar"`', + 'Invalid jmespath expression: parse error at column 20, found unexpected token "bar" (literal) in expression: twolen[].threelen[].`"bar"`', }, ])('filter errors', ({ expression, error }) => { // Prepare diff --git a/packages/jmespath/tests/unit/unicode.test.ts b/packages/jmespath/tests/unit/unicode.test.ts index 6c07fb3e4a..e358e6d671 100644 --- a/packages/jmespath/tests/unit/unicode.test.ts +++ b/packages/jmespath/tests/unit/unicode.test.ts @@ -32,7 +32,7 @@ describe('Unicode tests', () => { }, { expression: '"☃"', - expected: undefined, + expected: null, }, ])( 'should parse an object with unicode chars as keys', From 4debde42bf6be72a18ed506056689e7b0f6fee11 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sun, 25 Jun 2023 14:07:22 +0000 Subject: [PATCH 054/103] chore: lock --- package-lock.json | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa8a71ebcb..48e25c231d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,7 @@ "docs/snippets", "layers", "examples/cdk", - "examples/sam", - "packages/jmespath" + "examples/sam" ], "devDependencies": { "@middy/core": "^4.7.0", @@ -293,10 +292,6 @@ "resolved": "packages/idempotency", "link": true }, - "node_modules/@aws-lambda-powertools/jmespath": { - "resolved": "packages/jmespath", - "link": true - }, "node_modules/@aws-lambda-powertools/logger": { "resolved": "packages/logger", "link": true @@ -17207,11 +17202,6 @@ } } }, - "packages/jmespath": { - "version": "1.8.0-alpha.0", - "license": "MIT-0", - "devDependencies": {} - }, "packages/logger": { "name": "@aws-lambda-powertools/logger", "version": "2.0.2", From b84e543c15e620529f88e013131a7b48fc3693fa Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sun, 25 Jun 2023 14:11:55 +0000 Subject: [PATCH 055/103] chore: package.json --- packages/jmespath/package.json | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/jmespath/package.json b/packages/jmespath/package.json index 53c2a3c479..7a733a148e 100644 --- a/packages/jmespath/package.json +++ b/packages/jmespath/package.json @@ -1,24 +1,22 @@ { "name": "@aws-lambda-powertools/jmespath", - "version": "1.8.0-alpha.0", - "description": "The jmespath package for the Powertools for AWS Lambda (TypeScript) library", + "version": "1.10.0", + "description": "The JMESPath package for the Powertools for AWS Lambda (TypeScript) library", "author": { "name": "Amazon Web Services", "url": "https://aws.amazon.com" }, - "publishConfig": { - "access": "public" - }, + "private": true, "scripts": { "test": "npm run test:unit", "test:unit": "jest --group=unit --detectOpenHandles --coverage --verbose", - "test:e2e:nodejs14x": "RUNTIME=nodejs14x jest --group=e2e", - "test:e2e:nodejs16x": "RUNTIME=nodejs16x jest --group=e2e", - "test:e2e:nodejs18x": "RUNTIME=nodejs18x jest --group=e2e", - "test:e2e": "jest --group=e2e", + "test:e2e": "echo 'Not applicable for this package'", + "watch": "jest --watch", "build": "tsc", "lint": "eslint --ext .ts,.js --no-error-on-unmatched-pattern .", - "lint-fix": "eslint --fix --ext .ts,.js --no-error-on-unmatched-pattern ." + "lint-fix": "eslint --fix --ext .ts,.js --no-error-on-unmatched-pattern .", + "prebuild": "rimraf ./lib", + "prepack": "node ../../.github/scripts/release_patch_package_json.js ." }, "lint-staged": { "*.ts": "npm run lint-fix", @@ -32,17 +30,17 @@ }, "main": "./lib/index.js", "types": "./lib/index.d.ts", - "homepage": "https://github.com/awslabs/aws-lambda-powertools-typescript/tree/main/packages/jmespath#readme", + "homepage": "https://github.com/aws-powertools/powertools-lambda-typescript/tree/main/packages/jmespath#readme", "license": "MIT-0", "files": [ "lib" ], "repository": { "type": "git", - "url": "git+https://github.com/awslabs/aws-lambda-powertools-typescript.git" + "url": "git+https://github.com/aws-powertools/powertools-lambda-typescript.git" }, "bugs": { - "url": "https://github.com/awslabs/aws-lambda-powertools-typescript/issues" + "url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues" }, "keywords": [ "aws", @@ -53,4 +51,4 @@ "serverless", "nodejs" ] -} \ No newline at end of file +} From c38ecc066f47f466376594e1a450788fe63bf1e2 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sun, 25 Jun 2023 17:23:04 +0000 Subject: [PATCH 056/103] feat: handle negative indexes --- packages/jmespath/src/visitor/TreeInterpreter.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/jmespath/src/visitor/TreeInterpreter.ts b/packages/jmespath/src/visitor/TreeInterpreter.ts index a2da3cd8b1..e3328d4104 100644 --- a/packages/jmespath/src/visitor/TreeInterpreter.ts +++ b/packages/jmespath/src/visitor/TreeInterpreter.ts @@ -304,7 +304,8 @@ class TreeInterpreter { if (typeof node.value !== 'number') { throw new Error(`Invalid index: ${node.value}`); } - const found = value[node.value]; + const index = node.value < 0 ? value.length + node.value : node.value; + const found = value[index]; if (found === undefined) { return null; } From 0f99c294e3dc7428e4a93b567ebe91e606796abe Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 3 Aug 2023 10:29:25 +0000 Subject: [PATCH 057/103] feat: filters and literal compliance --- packages/jmespath/src/Lexer.ts | 4 +- .../jmespath/src/visitor/TreeInterpreter.ts | 8 +- packages/jmespath/src/visitor/utils.ts | 53 +++- .../helpers/populateEnvironmentVariables.ts | 2 +- packages/jmespath/tests/unit/base.test.ts | 25 +- packages/jmespath/tests/unit/boolean.test.ts | 66 ++--- packages/jmespath/tests/unit/current.test.ts | 25 +- packages/jmespath/tests/unit/escape.test.ts | 37 +-- packages/jmespath/tests/unit/filters.test.ts | 245 ++++++++++-------- .../jmespath/tests/unit/identifiers.test.ts | 2 +- packages/jmespath/tests/unit/indices.test.ts | 33 +-- packages/jmespath/tests/unit/literal.test.ts | 68 ++--- .../jmespath/tests/unit/multiselect.test.ts | 39 +-- packages/jmespath/tests/unit/pipe.test.ts | 6 +- packages/jmespath/tests/unit/syntax.test.ts | 177 +++++++------ packages/jmespath/tests/unit/unicode.test.ts | 6 +- packages/jmespath/tests/unit/wildcard.test.ts | 73 +++--- 17 files changed, 492 insertions(+), 377 deletions(-) diff --git a/packages/jmespath/src/Lexer.ts b/packages/jmespath/src/Lexer.ts index 3f2bfd3eff..dab1d0e313 100644 --- a/packages/jmespath/src/Lexer.ts +++ b/packages/jmespath/src/Lexer.ts @@ -17,7 +17,7 @@ class Lexer { public *tokenize(expression: string): Generator { this.#initializeForExpression(expression); - while (this.#current !== '') { + while (this.#current !== '' && this.#current !== undefined) { if (SIMPLE_TOKENS.has(this.#current)) { yield { // We know that SIMPLE_TOKENS has this.#current as a key because @@ -265,7 +265,7 @@ class Lexer { */ #consumeRawStringLiteral(): Token { const start = this.#position; - const lexeme = this.#consumeUntil('"').replace(`\\'`, `'`); + const lexeme = this.#consumeUntil(`'`).replace(`\\'`, `'`); const tokenLen = this.#position - start; return { diff --git a/packages/jmespath/src/visitor/TreeInterpreter.ts b/packages/jmespath/src/visitor/TreeInterpreter.ts index e3328d4104..89abfd544f 100644 --- a/packages/jmespath/src/visitor/TreeInterpreter.ts +++ b/packages/jmespath/src/visitor/TreeInterpreter.ts @@ -1,6 +1,6 @@ import type { JSONValue, Node, TreeInterpreterOptions } from '../types'; import { Functions } from '../functions'; -import { Expression, isRecord, isTruthy } from './utils'; +import { Expression, isRecord, isStrictEqual, isTruthy } from './utils'; import { ArityError, JMESPathTypeError, @@ -135,9 +135,9 @@ class TreeInterpreter { ) { // Common cases: comparator is == or != if (comparator === 'eq') { - return left === right; + return isStrictEqual(left, right); } else if (comparator === 'ne') { - return left !== right; + return !isStrictEqual(left, right); } else if (typeof left === 'number' && typeof right === 'number') { // Ordering operators only work on numbers. Evaluating them on other // types will return null. @@ -356,7 +356,7 @@ class TreeInterpreter { * @returns */ #visitLiteral(node: Node, _value: JSONValue): JSONValue { - return node.value || null; + return node.value; } /** diff --git a/packages/jmespath/src/visitor/utils.ts b/packages/jmespath/src/visitor/utils.ts index 8c74a7dca4..22a29ce788 100644 --- a/packages/jmespath/src/visitor/utils.ts +++ b/packages/jmespath/src/visitor/utils.ts @@ -41,7 +41,7 @@ const isTruthy = (value: unknown): boolean => { if (typeof value === 'string') { return value !== ''; } else if (typeof value === 'number') { - return value !== 0; + return true; } else if (typeof value === 'boolean') { return value; } else if (Array.isArray(value)) { @@ -53,4 +53,53 @@ const isTruthy = (value: unknown): boolean => { } }; -export { Expression, isRecord, isTruthy }; +/** + * Check if two unknown values are strictly equal. + * + * If the values are arrays, then each element is compared, regardless of + * order. If the values are objects, then each key and value from left + * is compared to the corresponding key and value from right. If the + * values are primitives, then they are compared using strict equality. + * + * @param left Left side of strict equality comparison + * @param right Right side of strict equality comparison + * @returns True if the values are strictly equal, false otherwise + */ +const isStrictEqual = (left: unknown, right: unknown): boolean => { + if (left === right) { + return true; + } else if (typeof left !== typeof right) { + return false; + } else if (Array.isArray(left) && Array.isArray(right)) { + if (left.length !== right.length) { + return false; + } + for (const [i, value] of left.entries()) { + if (!isStrictEqual(value, right[i])) { + return false; + } + } + + return true; + } else if (isRecord(left) && isRecord(right)) { + const leftKeys = Object.keys(left); + const leftValues = Object.values(left); + const rightKeys = Object.keys(right); + const rightValues = Object.values(right); + if ( + leftKeys.length !== rightKeys.length || + leftValues.length !== rightValues.length + ) { + return false; + } + + return ( + isStrictEqual(leftKeys, rightKeys) && + isStrictEqual(leftValues, rightValues) + ); + } else { + return false; + } +}; + +export { Expression, isRecord, isTruthy, isStrictEqual }; diff --git a/packages/jmespath/tests/helpers/populateEnvironmentVariables.ts b/packages/jmespath/tests/helpers/populateEnvironmentVariables.ts index 18f29d6992..a6715813ef 100644 --- a/packages/jmespath/tests/helpers/populateEnvironmentVariables.ts +++ b/packages/jmespath/tests/helpers/populateEnvironmentVariables.ts @@ -1 +1 @@ -// Powertools for AWS Lambda (TypeScript) variables \ No newline at end of file +// Powertools for AWS Lambda (TypeScript) variables diff --git a/packages/jmespath/tests/unit/base.test.ts b/packages/jmespath/tests/unit/base.test.ts index f4942363fc..a64655f88f 100644 --- a/packages/jmespath/tests/unit/base.test.ts +++ b/packages/jmespath/tests/unit/base.test.ts @@ -43,16 +43,19 @@ describe('Base tests', () => { expression: 'bad.morebad.morebad', expected: null, }, - ])('should parse a multi-level nested object', ({ expression, expected }) => { - // Prepare - const data = { foo: { bar: { baz: 'correct' } } }; + ])( + 'should parse a multi-level nested object: $expression', + ({ expression, expected }) => { + // Prepare + const data = { foo: { bar: { baz: 'correct' } } }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -64,7 +67,7 @@ describe('Base tests', () => { expected: ['one', 'two', 'three'], }, ])( - 'should parse multi-level objects with arrays', + 'should parse multi-level objects with arrays: $expression', ({ expression, expected }) => { // Prepare const data = { foo: { bar: ['one', 'two', 'three'] } }; @@ -94,7 +97,7 @@ describe('Base tests', () => { expression: 'one.two', expected: null, }, - ])('should parse an array', ({ expression, expected }) => { + ])('should parse an array: $expression', ({ expression, expected }) => { // Prepare const data = ['one', 'two', 'three']; @@ -119,7 +122,7 @@ describe('Base tests', () => { expected: 'bar', }, ])( - 'should parse an object with arrays and numeric values as keys', + 'should parse an object with arrays and numeric values as keys: $expression', ({ expression, expected }) => { // Prepare const data = { foo: { '1': ['one', 'two', 'three'], '-1': 'bar' } }; diff --git a/packages/jmespath/tests/unit/boolean.test.ts b/packages/jmespath/tests/unit/boolean.test.ts index 55856b4ff9..ea0fe8f749 100644 --- a/packages/jmespath/tests/unit/boolean.test.ts +++ b/packages/jmespath/tests/unit/boolean.test.ts @@ -47,22 +47,25 @@ describe('Boolean tests', () => { expression: 'outer.bad||outer.alsobad', expected: null, }, - ])('should support boolean OR comparison', ({ expression, expected }) => { - // Prepare - const data = { - outer: { - foo: 'foo', - bar: 'bar', - baz: 'baz', - }, - }; + ])( + 'should support boolean OR comparison: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + outer: { + foo: 'foo', + bar: 'bar', + baz: 'baz', + }, + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -75,7 +78,7 @@ describe('Boolean tests', () => { expected: 'foo', }, ])( - 'should support multiple boolean OR comparisons', + 'should support multiple boolean OR comparisons: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -216,22 +219,25 @@ describe('Boolean tests', () => { expression: '!!Zero', expected: true, }, - ])('should support boolean AND comparison', ({ expression, expected }) => { - // Prepare - const data = { - True: true, - False: false, - Number: 5, - EmptyList: [], - Zero: 0, - }; + ])( + 'should support boolean AND comparison: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + True: true, + False: false, + Number: 5, + EmptyList: [], + Zero: 0, + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -279,7 +285,7 @@ describe('Boolean tests', () => { expected: false, }, ])( - 'should support lesser and equal comparison', + 'should support lesser and equal comparison: $expression', ({ expression, expected }) => { // Prepare const data = { diff --git a/packages/jmespath/tests/unit/current.test.ts b/packages/jmespath/tests/unit/current.test.ts index 67e62b1655..29aebd0985 100644 --- a/packages/jmespath/tests/unit/current.test.ts +++ b/packages/jmespath/tests/unit/current.test.ts @@ -22,17 +22,20 @@ describe('Current operator tests', () => { expression: '@.foo[0]', expected: { name: 'a' }, }, - ])('should support the current operator', ({ expression, expected }) => { - // Prepare - const data = { - foo: [{ name: 'a' }, { name: 'b' }], - bar: { baz: 'qux' }, - }; + ])( + 'should support the current operator: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [{ name: 'a' }, { name: 'b' }], + bar: { baz: 'qux' }, + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); }); diff --git a/packages/jmespath/tests/unit/escape.test.ts b/packages/jmespath/tests/unit/escape.test.ts index 612843be91..a20cea2a05 100644 --- a/packages/jmespath/tests/unit/escape.test.ts +++ b/packages/jmespath/tests/unit/escape.test.ts @@ -39,23 +39,26 @@ describe('Escape characters tests', () => { expression: '"bar"."baz"', expected: 'qux', }, - ])('should support escaping characters', ({ expression, expected }) => { - // Prepare - const data = { - 'foo.bar': 'dot', - 'foo bar': 'space', - 'foo\nbar': 'newline', - 'foo"bar': 'doublequote', - 'c:\\\\windows\\path': 'windows', - '/unix/path': 'unix', - '"""': 'threequotes', - bar: { baz: 'qux' }, - }; + ])( + 'should support escaping characters: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + 'foo.bar': 'dot', + 'foo bar': 'space', + 'foo\nbar': 'newline', + 'foo"bar': 'doublequote', + 'c:\\\\windows\\path': 'windows', + '/unix/path': 'unix', + '"""': 'threequotes', + bar: { baz: 'qux' }, + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); }); diff --git a/packages/jmespath/tests/unit/filters.test.ts b/packages/jmespath/tests/unit/filters.test.ts index 602d2d1700..aa14b9f6a7 100644 --- a/packages/jmespath/tests/unit/filters.test.ts +++ b/packages/jmespath/tests/unit/filters.test.ts @@ -12,7 +12,7 @@ describe('Filer operator tests', () => { expression: `foo[?name == 'a']`, expected: [{ name: 'a' }], }, - ])('should match a literal', ({ expression, expected }) => { + ])('should match a literal: $expression', ({ expression, expected }) => { // Prepare const data = { foo: [{ name: 'a' }, { name: 'b' }] }; @@ -28,16 +28,19 @@ describe('Filer operator tests', () => { expression: '*[?[0] == `0`]', expected: [[], []], }, - ])('should match a literal in arrays', ({ expression, expected }) => { - // Prepare - const data = { foo: [0, 1], bar: [2, 3] }; + ])( + 'should match a literal in arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data = { foo: [0, 1], bar: [2, 3] }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -49,7 +52,7 @@ describe('Filer operator tests', () => { expression: 'foo[?first == last].first', expected: ['foo'], }, - ])('should match an expression', ({ expression, expected }) => { + ])('should match an expression: $expression', ({ expression, expected }) => { // Prepare const data = { foo: [ @@ -105,7 +108,7 @@ describe('Filer operator tests', () => { expected: [{ age: 25 }, { age: 30 }], }, ])( - 'should match an expression with operators', + 'should match an expression with operators: $expression', ({ expression, expected }) => { // Prepare const data = { foo: [{ age: 20 }, { age: 25 }, { age: 30 }] }; @@ -157,7 +160,7 @@ describe('Filer operator tests', () => { expected: [{ weight: 44.4 }, { weight: 55.5 }], }, ])( - 'should match an expression with comparisons', + 'should match an expression with comparisons: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -177,22 +180,19 @@ describe('Filer operator tests', () => { expression: `foo[?top.name == 'a']`, expected: [{ top: { name: 'a' } }], }, - ])('should match with subexpression', ({ expression, expected }) => { - // Prepare - const data = { - foo: [ - { first: 'foo', last: 'bar' }, - { first: 'foo', last: 'foo' }, - { first: 'foo', last: 'baz' }, - ], - }; + ])( + 'should match with subexpression: $expression', + ({ expression, expected }) => { + // Prepare + const data = { foo: [{ top: { name: 'a' } }, { top: { name: 'b' } }] }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -205,7 +205,7 @@ describe('Filer operator tests', () => { expression: 'foo[?top == `{"first": "foo", "last": "bar"}`]', expected: [{ top: { first: 'foo', last: 'bar' } }], }, - ])('should match with arrays', ({ expression, expected }) => { + ])('should match with arrays: $expression', ({ expression, expected }) => { // Prepare const data = { foo: [ @@ -478,7 +478,7 @@ describe('Filer operator tests', () => { ], }, ])( - 'should match with object that have mixed types as values', + 'should match with object that have mixed types as values: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -563,31 +563,34 @@ describe('Filer operator tests', () => { expression: 'foo[? `false`]', expected: [], }, - ])('should match with falsy values', ({ expression, expected }) => { - // Prepare - const data = { - foo: [ - { key: true }, - { key: false }, - { key: 0 }, - { key: 0.0 }, - { key: 1 }, - { key: 1.0 }, - { key: [0] }, - { key: null }, - { key: [1] }, - { key: [] }, - { key: {} }, - { key: { a: 2 } }, - ], - }; + ])( + 'should match with falsy values: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 0.0 }, + { key: 1 }, + { key: 1.0 }, + { key: [0] }, + { key: null }, + { key: [1] }, + { key: [] }, + { key: {} }, + { key: { a: 2 } }, + ], + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -603,7 +606,7 @@ describe('Filer operator tests', () => { expected: [{ foo: 2, bar: 1 }], }, ])( - 'should match with nested objects and arrays', + 'should match with nested objects and arrays: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -633,7 +636,7 @@ describe('Filer operator tests', () => { expected: [], }, ])( - 'should match with nested objects and arrays with different structures', + 'should match with nested objects and arrays with different structures: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -660,24 +663,27 @@ describe('Filer operator tests', () => { expression: 'foo[?a==`1`].b.c', expected: ['x', 'y', 'z'], }, - ])('should support filter in indexes', ({ expression, expected }) => { - // Prepare - const data = { - foo: [ - { a: 1, b: { c: 'x' } }, - { a: 1, b: { c: 'y' } }, - { a: 1, b: { c: 'z' } }, - { a: 2, b: { c: 'z' } }, - { a: 1, baz: 2 }, - ], - }; + ])( + 'should support filter in indexes: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { a: 1, b: { c: 'x' } }, + { a: 1, b: { c: 'y' } }, + { a: 1, b: { c: 'z' } }, + { a: 2, b: { c: 'z' } }, + { a: 1, baz: 2 }, + ], + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -694,7 +700,7 @@ describe('Filer operator tests', () => { expected: [{ name: 'a' }, { name: 'b' }, { name: 'c' }], }, ])( - 'should support filter with or expressions', + 'should support filter with or expressions: $expression', ({ expression, expected }) => { // Prepare const data = { foo: [{ name: 'a' }, { name: 'b' }, { name: 'c' }] }; @@ -717,21 +723,24 @@ describe('Filer operator tests', () => { expression: 'foo[?a == `1` && b == `4`]', expected: [], }, - ])('should support filter and expressions', ({ expression, expected }) => { - // Prepare - const data = { - foo: [ - { a: 1, b: 2 }, - { a: 1, b: 3 }, - ], - }; + ])( + 'should support filter and expressions: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { a: 1, b: 2 }, + { a: 1, b: 3 }, + ], + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -811,7 +820,7 @@ describe('Filer operator tests', () => { expected: [{ a: 3, b: 4 }], }, ])( - 'should support filter with expressions and respect precedence', + 'should support filter with expressions and respect precedence: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -857,29 +866,32 @@ describe('Filer operator tests', () => { expression: 'foo[?key == `null`]', expected: [{ key: null }, { notkey: true }], }, - ])('should support unary expressions', ({ expression, expected }) => { - // Prepare - const data = { - foo: [ - { key: true }, - { key: false }, - { key: [] }, - { key: {} }, - { key: [0] }, - { key: { a: 'b' } }, - { key: 0 }, - { key: 1 }, - { key: null }, - { notkey: true }, - ], - }; + ])( + 'should support unary expressions: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { key: true }, + { key: false }, + { key: [] }, + { key: {} }, + { key: [0] }, + { key: { a: 'b' } }, + { key: 0 }, + { key: 1 }, + { key: null }, + { notkey: true }, + ], + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -897,16 +909,19 @@ describe('Filer operator tests', () => { expression: 'foo[?@ == @]', expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], }, - ])('should support using current in a filter', ({ expression, expected }) => { - // Prepare - const data = { - foo: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - }; + ])( + 'should support using current in a filter: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); }); diff --git a/packages/jmespath/tests/unit/identifiers.test.ts b/packages/jmespath/tests/unit/identifiers.test.ts index 993e26ab8e..d1e19b8142 100644 --- a/packages/jmespath/tests/unit/identifiers.test.ts +++ b/packages/jmespath/tests/unit/identifiers.test.ts @@ -883,7 +883,7 @@ describe('Identifiers tests', () => { expected: true, }, ])( - 'should handle different identifiers', + 'should handle different identifiers: $expression', ({ data, expression, expected }) => { // Act const result = search(expression, data); diff --git a/packages/jmespath/tests/unit/indices.test.ts b/packages/jmespath/tests/unit/indices.test.ts index 8a8e16a549..c02718b594 100644 --- a/packages/jmespath/tests/unit/indices.test.ts +++ b/packages/jmespath/tests/unit/indices.test.ts @@ -40,7 +40,7 @@ describe('Indices tests', () => { expected: null, }, ])( - 'should support indices on arrays in a nested object', + 'should support indices on arrays in a nested object: $expression', ({ expression, expected }) => { // Prepare const data = { foo: { bar: ['zero', 'one', 'two'] } }; @@ -99,7 +99,7 @@ describe('Indices tests', () => { expected: null, }, ])( - 'should support indices in an array with objects inside', + 'should support indices in an array with objects inside: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -144,16 +144,19 @@ describe('Indices tests', () => { expression: '[-3]', expected: 'one', }, - ])('should support indices in an array', ({ expression, expected }) => { - // Prepare - const data = ['one', 'two', 'three']; + ])( + 'should support indices in an array: $expression', + ({ expression, expected }) => { + // Prepare + const data = ['one', 'two', 'three']; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -173,7 +176,7 @@ describe('Indices tests', () => { expected: [], }, ])( - 'should support indices in multi-level nested arrays & objects', + 'should support indices in multi-level nested arrays & objects: $expression', ({ expression, expected }) => { // Prepare const data = { reservations: [{ instances: [{ foo: 1 }, { foo: 2 }] }] }; @@ -228,7 +231,7 @@ describe('Indices tests', () => { expected: [1, 2, 3, 4, 1, 2, 3, 4], }, ])( - 'should support indices in large mixed objects and arrays', + 'should support indices in large mixed objects and arrays: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -338,7 +341,7 @@ describe('Indices tests', () => { expected: [], }, ])( - 'should support indices in objects containing an array of matrixes', + 'should support indices in objects containing an array of matrixes: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -425,7 +428,7 @@ describe('Indices tests', () => { expected: [1, 3, 5, 7], }, ])( - 'should support indices with nested arrays and objects at different levels', + 'should support indices with nested arrays and objects at different levels: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -503,7 +506,7 @@ describe('Indices tests', () => { expected: null, }, ])( - 'should support indices in objects having special names as keys', + 'should support indices in objects having special names as keys: $expression', ({ expression, expected }) => { // Prepare const data = { diff --git a/packages/jmespath/tests/unit/literal.test.ts b/packages/jmespath/tests/unit/literal.test.ts index 8036ad544a..ffc68d6cb8 100644 --- a/packages/jmespath/tests/unit/literal.test.ts +++ b/packages/jmespath/tests/unit/literal.test.ts @@ -118,28 +118,31 @@ describe('Literal expressions tests', () => { expression: '`[0, 1, 2]`[1]', expected: 1, }, - ])('should support literal expressions', ({ expression, expected }) => { - // Prepare - const data = { - foo: [ - { - name: 'a', - }, - { - name: 'b', + ])( + 'should support literal expressions: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { + name: 'a', + }, + { + name: 'b', + }, + ], + bar: { + baz: 'qux', }, - ], - bar: { - baz: 'qux', - }, - }; + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -157,7 +160,7 @@ describe('Literal expressions tests', () => { }, }, ])( - 'should support literals with other special characters', + 'should support literals with other special characters: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -176,10 +179,10 @@ describe('Literal expressions tests', () => { { comment: 'Literal on RHS of subexpr not allowed', expression: 'foo.`"bar"`', - error: 'Syntax error, unexpected token: bar(Literal)', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "bar" (literal) in expression: foo.`"bar"`', }, - ])('literals errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in literal errors errors tests + ])('literals errors: $expression', ({ expression, error }) => { // Prepare const data = { type: 'object', @@ -236,14 +239,17 @@ describe('Literal expressions tests', () => { expression: `'foo\\'bar'`, expected: `foo'bar`, }, - ])('should support raw string literals', ({ expression, expected }) => { - // Prepare - const data = {}; + ])( + 'should support raw string literals: $expression', + ({ expression, expected }) => { + // Prepare + const data = {}; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); }); diff --git a/packages/jmespath/tests/unit/multiselect.test.ts b/packages/jmespath/tests/unit/multiselect.test.ts index 0a969f6b4b..4d2697fb1a 100644 --- a/packages/jmespath/tests/unit/multiselect.test.ts +++ b/packages/jmespath/tests/unit/multiselect.test.ts @@ -108,7 +108,7 @@ describe('Multiselect expressions tests', () => { expected: [null, null], }, ])( - 'should support expression on large nested objects', + 'should support expression on large nested objects: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -173,7 +173,7 @@ describe('Multiselect expressions tests', () => { expected: [null, null], }, ])( - 'should support the expression on objects containing arrays', + 'should support the expression on objects containing arrays: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -198,7 +198,7 @@ describe('Multiselect expressions tests', () => { expected: [1, 2], }, ])( - 'should support the expression using both array and object syntax', + 'should support the expression using both array and object syntax: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -235,7 +235,7 @@ describe('Multiselect expressions tests', () => { expected: [true, ['first', 'second']], }, ])( - 'should support the expression using mixed array and object syntax', + 'should support the expression using mixed array and object syntax: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -292,7 +292,7 @@ describe('Multiselect expressions tests', () => { ], }, ])( - 'should support the expression with wildcards', + 'should support the expression with wildcards: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -395,7 +395,7 @@ describe('Multiselect expressions tests', () => { expected: [1, 2, 3, 4, 5, 6, 7, 8], }, ])( - 'should support expression with the flatten operator', + 'should support expression with the flatten operator: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -441,7 +441,7 @@ describe('Multiselect expressions tests', () => { expected: [['abc', 'def'], 'zero'], }, ])( - 'should support the expression with slicing', + 'should support the expression with slicing: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -478,7 +478,7 @@ describe('Multiselect expressions tests', () => { ], }, ])( - 'should support the expression with wildcard slicing', + 'should support the expression with wildcard slicing: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -513,7 +513,7 @@ describe('Multiselect expressions tests', () => { expected: [['a', 'd'], 'zero'], }, ])( - 'should support multiselect with inexistent values', + 'should support multiselect with inexistent values: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -548,16 +548,19 @@ describe('Multiselect expressions tests', () => { expression: '[[*],*]', expected: [null, ['object']], }, - ])('should support nested multiselect', ({ expression, expected }) => { - // Prepare - const data = { type: 'object' }; + ])( + 'should support nested multiselect: $expression', + ({ expression, expected }) => { + // Prepare + const data = { type: 'object' }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -565,7 +568,7 @@ describe('Multiselect expressions tests', () => { expected: [[]], }, ])( - 'should handle nested multiselect with empty arrays', + 'should handle nested multiselect with empty arrays: $expression', ({ expression, expected }) => { // Prepare const data: string[] = []; diff --git a/packages/jmespath/tests/unit/pipe.test.ts b/packages/jmespath/tests/unit/pipe.test.ts index e9a51f8f8f..943e0b0f21 100644 --- a/packages/jmespath/tests/unit/pipe.test.ts +++ b/packages/jmespath/tests/unit/pipe.test.ts @@ -35,7 +35,7 @@ describe('Pipe expressions tests', () => { expected: ['subkey', 'subkey'], }, ])( - 'should support piping a multi-level nested object with arrays', + 'should support piping a multi-level nested object with arrays: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -108,7 +108,7 @@ describe('Pipe expressions tests', () => { expected: { baz: 'one' }, }, ])( - 'should support piping with boolean conditions', + 'should support piping with boolean conditions: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -149,7 +149,7 @@ describe('Pipe expressions tests', () => { expected: null, }, ])( - 'should support piping with wildcard and current operators', + 'should support piping with wildcard and current operators: $expression', ({ expression, expected }) => { // Prepare const data = { diff --git a/packages/jmespath/tests/unit/syntax.test.ts b/packages/jmespath/tests/unit/syntax.test.ts index 82bcf96e86..c38f8afc72 100644 --- a/packages/jmespath/tests/unit/syntax.test.ts +++ b/packages/jmespath/tests/unit/syntax.test.ts @@ -15,7 +15,7 @@ describe('Syntax tests', () => { expression: 'foo', expected: null, }, - ])('should support dot syntax', ({ expression, expected }) => { + ])('should support dot syntax: $expression', ({ expression, expected }) => { // Prepare const data = { type: 'object', @@ -64,7 +64,7 @@ describe('Syntax tests', () => { error: 'Invalid jmespath expression: parse error at column 4, found unexpected token "." (dot) in expression: foo[.]', }, - ])('dot syntax errors', ({ expression, error }) => { + ])('dot syntax errors: $expression', ({ expression, error }) => { // Prepare const data = { type: 'object', @@ -145,7 +145,7 @@ describe('Syntax tests', () => { error: 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) in expression: !', }, - ])('simple token errors', ({ expression, error }) => { + ])('simple token errors: $expression', ({ expression, error }) => { // Prepare const data = { type: 'object', @@ -161,7 +161,7 @@ describe('Syntax tests', () => { error: 'Invalid jmespath expression: parse error at column 5, found unexpected end of expression (EOF) in expression: ![!(!', }, - ])('boolean token errors', ({ expression, error }) => { + ])('boolean token errors: $expression', ({ expression, error }) => { // Prepare const data = { type: 'object', @@ -188,18 +188,21 @@ describe('Syntax tests', () => { expression: '*[0]', expected: [], }, - ])('should support wildcard syntax', ({ expression, expected }) => { - // Prepare - const data = { - type: 'object', - }; + ])( + 'should support wildcard syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -227,7 +230,7 @@ describe('Syntax tests', () => { error: 'Invalid jmespath expression: parse error at column 6, found unexpected token "*" (star) in expression: foo[*]*', }, - ])('wildcard token errors', ({ expression, error }) => { + ])('wildcard token errors: $expression', ({ expression, error }) => { // Prepare const data = { type: 'object', @@ -242,18 +245,21 @@ describe('Syntax tests', () => { expression: '[]', expected: null, }, - ])('should support flatten syntax', ({ expression, expected }) => { - // Prepare - const data = { - type: 'object', - }; + ])( + 'should support flatten syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -276,7 +282,7 @@ describe('Syntax tests', () => { expression: '[*][0]', expected: null, }, - ])('simple bracket syntax', ({ expression, expected }) => { + ])('simple bracket syntax: $expression', ({ expression, expected }) => { // Prepare const data = { type: 'object', @@ -300,7 +306,7 @@ describe('Syntax tests', () => { error: 'Bad jmespath expression: unknown token "#" at column 4 in expression: foo[#]', }, - ])('simple breacket errors', ({ expression, error }) => { + ])('simple breacket errors: $expression', ({ expression, error }) => { // Prepare const data = { type: 'object', @@ -329,18 +335,21 @@ describe('Syntax tests', () => { expression: 'foo.[abc, def]', expected: null, }, - ])('should support multi-select list syntax', ({ expression, expected }) => { - // Prepare - const data = { - type: 'object', - }; + ])( + 'should support multi-select list syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -427,7 +436,7 @@ describe('Syntax tests', () => { error: 'Invalid jmespath expression: parse error at column 5, found unexpected token "0" (number) in expression: foo.[0, 1]', }, - ])('multi-select list errors', ({ expression, error }) => { + ])('multi-select list errors: $expression', ({ expression, error }) => { // Prepare const data = { type: 'object', @@ -457,16 +466,19 @@ describe('Syntax tests', () => { }, }, }, - ])('should support multy-select hash syntax', ({ expression, expected }) => { - // Prepare - const data = { type: 'object' }; + ])( + 'should support multy-select hash syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data = { type: 'object' }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -576,7 +588,7 @@ describe('Syntax tests', () => { error: 'Invalid jmespath expression: parse error at column 23, found unexpected token "}" (rbrace) in expression: a.{foo: bar, baz: bam, }', }, - ])('multi-select hash errors', ({ expression, error }) => { + ])('multi-select hash errors: $expression', ({ expression, error }) => { // Prepare const data = { type: 'object', @@ -595,18 +607,21 @@ describe('Syntax tests', () => { expression: 'foo.[a || b]', expected: null, }, - ])('should support boolean OR syntax', ({ expression, expected }) => { - // Prepare - const data = { - type: 'object', - }; + ])( + 'should support boolean OR syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -639,7 +654,7 @@ describe('Syntax tests', () => { error: 'Bad jmespath expression: unknown token ""foo" at column 0 in expression: "foo', }, - ])('boolean OR errors', ({ expression, error }) => { + ])('boolean OR errors: $expression', ({ expression, error }) => { // Prepare const data = { type: 'object', @@ -685,18 +700,21 @@ describe('Syntax tests', () => { expression: '[?"\\\\" > `"foo"`]', expected: null, }, - ])('should support filter syntax', ({ expression, expected }) => { - // Prepare - const data = { - type: 'object', - }; + ])( + 'should support filter syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -806,7 +824,7 @@ describe('Syntax tests', () => { error: 'Invalid jmespath expression: parse error at column 20, found unexpected token "bar" (literal) in expression: twolen[].threelen[].`"bar"`', }, - ])('filter errors', ({ expression, error }) => { + ])('filter errors: $expression', ({ expression, error }) => { // Prepare const data = { type: 'object', @@ -829,7 +847,7 @@ describe('Syntax tests', () => { expression: '"\\\\"', expected: null, }, - ])('should support identifiers', ({ expression, expected }) => { + ])('should support identifiers: $expression', ({ expression, expected }) => { // Prepare const data = { type: 'object' }; @@ -853,14 +871,17 @@ describe('Syntax tests', () => { expression: '[*.*]', expected: [null], }, - ])('should support combined syntax', ({ expression, expected }) => { - // Prepare - const data: string[] = []; + ])( + 'should support combined syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data: string[] = []; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); }); diff --git a/packages/jmespath/tests/unit/unicode.test.ts b/packages/jmespath/tests/unit/unicode.test.ts index e358e6d671..8a841dabdd 100644 --- a/packages/jmespath/tests/unit/unicode.test.ts +++ b/packages/jmespath/tests/unit/unicode.test.ts @@ -12,7 +12,7 @@ describe('Unicode tests', () => { expected: ['✓', '✗'], }, ])( - 'should parse an object with unicode chars as keys and values', + 'should parse an object with unicode chars as keys and values: $expression', ({ expression, expected }) => { // Prepare const data = { foo: [{ '✓': '✓' }, { '✓': '✗' }] }; @@ -35,7 +35,7 @@ describe('Unicode tests', () => { expected: null, }, ])( - 'should parse an object with unicode chars as keys', + 'should parse an object with unicode chars as keys: $expression', ({ expression, expected }) => { // Prepare const data = { '☯': true }; @@ -54,7 +54,7 @@ describe('Unicode tests', () => { expected: true, }, ])( - 'should parse an object with mulitple unicode chars as keys', + 'should parse an object with mulitple unicode chars as keys: $expression', ({ expression, expected }) => { // Prepare const data = { '♪♫•*¨*•.¸¸❤¸¸.•*¨*•♫♪': true }; diff --git a/packages/jmespath/tests/unit/wildcard.test.ts b/packages/jmespath/tests/unit/wildcard.test.ts index eb46d1147b..8fabbd3077 100644 --- a/packages/jmespath/tests/unit/wildcard.test.ts +++ b/packages/jmespath/tests/unit/wildcard.test.ts @@ -31,7 +31,7 @@ describe('Wildcard tests', () => { expected: ['c', 'c'], }, ])( - 'should parse the wildcard operator with an object containing multiple keys at different levels', + 'should parse the wildcard operator with an object containing multiple keys at different levels: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -91,7 +91,7 @@ describe('Wildcard tests', () => { expected: [[], [], []], }, ])( - 'should parse the wildcard operator with an object containing keys with hyphens', + 'should parse the wildcard operator with an object containing keys with hyphens: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -122,7 +122,7 @@ describe('Wildcard tests', () => { expected: ['one', 'one'], }, ])( - 'should parse the wildcard operator with an object containing multiple keys', + 'should parse the wildcard operator with an object containing multiple keys: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -167,7 +167,7 @@ describe('Wildcard tests', () => { expected: ['one', 'one'], }, ])( - 'should parse the wildcard operator with an object containing nested objects', + 'should parse the wildcard operator with an object containing nested objects: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -197,7 +197,7 @@ describe('Wildcard tests', () => { expected: ['four'], }, ])( - 'should parse the wildcard operator with an object containing an array of objects', + 'should parse the wildcard operator with an object containing an array of objects: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -236,7 +236,7 @@ describe('Wildcard tests', () => { expected: ['four'], }, ])( - 'should parse the wildcard operator with an array of objects', + 'should parse the wildcard operator with an array of objects: $expression', ({ expression, expected }) => { // Prepare const data = [ @@ -280,7 +280,7 @@ describe('Wildcard tests', () => { expected: [], }, ])( - 'should parse the wildcard operator with an object with nested objects containing arrays', + 'should parse the wildcard operator with an object with nested objects containing arrays: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -330,7 +330,7 @@ describe('Wildcard tests', () => { expected: null, }, ])( - 'should parse the wildcard operator with an object with nested arrays', + 'should parse the wildcard operator with an object with nested arrays: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -363,7 +363,7 @@ describe('Wildcard tests', () => { expected: ['basic', 'advanced'], }, ])( - 'should parse the wildcard operator with an array of objects with nested arrays or strings', + 'should parse the wildcard operator with an array of objects with nested arrays or strings: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -388,7 +388,7 @@ describe('Wildcard tests', () => { expected: ['basic', 'intermediate', 'advanced', 'expert'], }, ])( - 'should parse the wildcard operator with an array of objects', + 'should parse the wildcard operator with an array of objects: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -423,7 +423,7 @@ describe('Wildcard tests', () => { expected: [], }, ])( - 'should parse the wildcard operator with an array of objects with arrays', + 'should parse the wildcard operator with an array of objects with arrays: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -448,7 +448,7 @@ describe('Wildcard tests', () => { expected: [], }, ])( - 'should parse the wildcard operator with an array of objects with empty arrays', + 'should parse the wildcard operator with an array of objects with empty arrays: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -473,7 +473,7 @@ describe('Wildcard tests', () => { expected: ['two', 'four'], }, ])( - 'should parse the wildcard operator with an array of arrays', + 'should parse the wildcard operator with an array of arrays: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -529,28 +529,31 @@ describe('Wildcard tests', () => { expression: 'bar[*].baz[*]', expected: null, }, - ])('should parse a nested array of arrays', ({ expression, expected }) => { - // Prepare - const data = { - foo: [ - [ - ['one', 'two'], - ['three', 'four'], - ], - [ - ['five', 'six'], - ['seven', 'eight'], + ])( + 'should parse a nested array of arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + [ + ['one', 'two'], + ['three', 'four'], + ], + [ + ['five', 'six'], + ['seven', 'eight'], + ], + [['nine'], ['ten']], ], - [['nine'], ['ten']], - ], - }; + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -590,7 +593,7 @@ describe('Wildcard tests', () => { expected: null, }, ])( - 'should parse an object with different value types', + 'should parse an object with different value types: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -630,7 +633,7 @@ describe('Wildcard tests', () => { expected: null, }, ])( - 'should parse an object with different value types', + 'should parse an object with different value types: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -649,7 +652,7 @@ describe('Wildcard tests', () => { } ); it.each([{ expression: '*[0]', expected: [0, 0] }])( - 'should get the first element of each array', + 'should get the first element of each array: $expression', ({ expression, expected }) => { // Prepare const data = { From a5e31ab44024613f7aeeffd44ebc12488aa725df Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 3 Aug 2023 10:30:16 +0000 Subject: [PATCH 058/103] chore: bumped --- packages/jmespath/package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/jmespath/package.json b/packages/jmespath/package.json index 7a733a148e..ade04f3d5a 100644 --- a/packages/jmespath/package.json +++ b/packages/jmespath/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/jmespath", - "version": "1.10.0", + "version": "1.12.1", "description": "The JMESPath package for the Powertools for AWS Lambda (TypeScript) library", "author": { "name": "Amazon Web Services", @@ -19,8 +19,7 @@ "prepack": "node ../../.github/scripts/release_patch_package_json.js ." }, "lint-staged": { - "*.ts": "npm run lint-fix", - "*.js": "npm run lint-fix" + "*.{ts,js}": "npm run lint-fix" }, "exports": { ".": { From f37686faa8f801bbef1c0e7148ac358643ffbe25 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 3 Aug 2023 14:38:56 +0000 Subject: [PATCH 059/103] feat: slice compliance --- .../jmespath/src/visitor/TreeInterpreter.ts | 36 ++++-- packages/jmespath/src/visitor/utils.ts | 114 +++++++++++++++++- packages/jmespath/tests/unit/slice.test.ts | 37 +++--- 3 files changed, 163 insertions(+), 24 deletions(-) diff --git a/packages/jmespath/src/visitor/TreeInterpreter.ts b/packages/jmespath/src/visitor/TreeInterpreter.ts index 89abfd544f..41c33e592c 100644 --- a/packages/jmespath/src/visitor/TreeInterpreter.ts +++ b/packages/jmespath/src/visitor/TreeInterpreter.ts @@ -1,6 +1,13 @@ import type { JSONValue, Node, TreeInterpreterOptions } from '../types'; import { Functions } from '../functions'; -import { Expression, isRecord, isStrictEqual, isTruthy } from './utils'; +import { + Expression, + isIntegerNumber, + isRecord, + isStrictEqual, + isTruthy, + sliceArray, +} from './utils'; import { ArityError, JMESPathTypeError, @@ -57,8 +64,8 @@ class TreeInterpreter { return this.#visitIndex(node, value); } else if (nodeType === 'index_expression') { return this.#visitIndexExpression(node, value); - /* } else if (nodeType === 'slice') { - return this.#visitSlice(node, value); */ + } else if (nodeType === 'slice') { + return this.#visitSlice(node, value); } else if (nodeType === 'key_val_pair') { return this.#visitKeyValPair(node, value); } else if (nodeType === 'literal') { @@ -334,10 +341,25 @@ class TreeInterpreter { * @param value * @returns */ - /* #visitSlice(node: Node, value: JSONValue): JSONValue { - - return true; - } */ + #visitSlice(node: Node, value: JSONValue): JSONValue { + const step = isIntegerNumber(node.children[2]) ? node.children[2] : 1; + if (step === 0) { + throw new Error('Invalid slice, step cannot be 0'); + } + if (!Array.isArray(value)) { + return null; + } + if (value.length === 0) { + return []; + } + + return sliceArray( + value, + node.children[0] as unknown as number, + node.children[1] as unknown as number, + step + ); + } /** * TODO: write docs for TreeInterpreter.visitKeyValPair() diff --git a/packages/jmespath/src/visitor/utils.ts b/packages/jmespath/src/visitor/utils.ts index 22a29ce788..da5059020b 100644 --- a/packages/jmespath/src/visitor/utils.ts +++ b/packages/jmespath/src/visitor/utils.ts @@ -102,4 +102,116 @@ const isStrictEqual = (left: unknown, right: unknown): boolean => { } }; -export { Expression, isRecord, isTruthy, isStrictEqual }; +/** + * Check if a value is a number. + * + * @param value The value to check + * @returns True if the value is a number, false otherwise + */ +const isNumber = (value: unknown): value is number => { + return typeof value === 'number'; +}; + +/** + * Check if a value is an integer number. + * + * @param value The value to check + * @returns True if the value is an integer number, false otherwise + */ +const isIntegerNumber = (value: unknown): value is number => { + return isNumber(value) && Number.isInteger(value); +}; + +/** + * @internal + * Cap a slice range value to the length of an array, taking into account + * negative values and whether the step is negative. + * + * @param arrayLength The length of the array + * @param value The value to cap + * @param isStepNegative Whether the step is negative + * @returns The capped value + */ +const capSliceRange = ( + arrayLength: number, + value: number, + isStepNegative: boolean +): number => { + if (value < 0) { + value += arrayLength; + if (value < 0) { + value = isStepNegative ? -1 : 0; + } + } else if (value >= arrayLength) { + value = isStepNegative ? arrayLength - 1 : arrayLength; + } + + return value; +}; + +/** + * Given a start, stop, and step value, the sub elements in an array are extracted as follows: + * * The first element in the extracted array is the index denoted by start. + * * The last element in the extracted array is the index denoted by end - 1. + * * The step value determines how many indices to skip after each element is selected from the array. An array of 1 (the default step) will not skip any indices. A step value of 2 will skip every other index while extracting elements from an array. A step value of -1 will extract values in reverse order from the array. + * + * Slice expressions adhere to the following rules: + * * If a negative start position is given, it is calculated as the total length of the array plus the given start position. + * * If no start position is given, it is assumed to be 0 if the given step is greater than 0 or the end of the array if the given step is less than 0. + * * If a negative stop position is given, it is calculated as the total length of the array plus the given stop position. + * * If no stop position is given, it is assumed to be the length of the array if the given step is greater than 0 or 0 if the given step is less than 0. + * * If the given step is omitted, it it assumed to be 1. + * * If the given step is 0, an invalid-value error MUST be raised (thrown before calling the function) + * * If the element being sliced is not an array, the result is null (returned before calling the function) + * * If the element being sliced is an array and yields no results, the result MUST be an empty array. + * + * @param array The array to slice + * @param start The start index + * @param end The end index + * @param step The step value + */ +const sliceArray = ( + array: T[], + start?: number, + end?: number, + step?: number +): T[] | null => { + step = isIntegerNumber(step) ? step : 1; + const isStepNegative = step < 0; + const length = array.length; + + start = isIntegerNumber(start) + ? capSliceRange(length, start, isStepNegative) + : isStepNegative + ? length - 1 + : 0; + + end = isIntegerNumber(end) + ? capSliceRange(length, end, isStepNegative) + : isStepNegative + ? -1 + : length; + + const result: T[] = []; + if (step > 0) { + for (let i = start; i < end; i += step) { + result.push(array[i]); + } + } else { + for (let i = start; i > end; i += step) { + result.push(array[i]); + } + } + + return result; +}; + +export { + Expression, + isRecord, + isTruthy, + isStrictEqual, + isNumber, + isIntegerNumber, + sliceArray, +}; diff --git a/packages/jmespath/tests/unit/slice.test.ts b/packages/jmespath/tests/unit/slice.test.ts index c18daedecd..ce639f7678 100644 --- a/packages/jmespath/tests/unit/slice.test.ts +++ b/packages/jmespath/tests/unit/slice.test.ts @@ -116,7 +116,7 @@ describe('Slices tests', () => { expected: [9, 8, 7, 6], }, ])( - 'should support slicing an object with arrays in it', + 'should support slicing arrays: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -141,26 +141,31 @@ describe('Slices tests', () => { }, { expression: 'foo[8:2:0:1]', - error: 'Expected Rbracket, got: Number', + error: + 'Invalid jmespath expression: parse error at column 9, found unexpected token ":" (colon) in expression: foo[8:2:0:1]', }, { expression: 'foo[8:2&]', - error: 'Syntax error, unexpected token: &(Expref)', + error: + 'Invalid jmespath expression: parse error at column 8, found unexpected token "]" (rbracket) in expression: foo[8:2&]', }, { expression: 'foo[2:a:3]', - error: 'Syntax error, unexpected token: a(UnquotedIdentifier)', - }, - ])('slicing objects with arrays errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in slicing objects with arrays errors tests - // Prepare - const data = { - type: 'object', - }; + error: + 'Invalid jmespath expression: parse error at column 6, found unexpected token "a" (unquoted_identifier) in expression: foo[2:a:3]', + }, + ])( + 'slicing objects with arrays errors: $expression', + ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; - // Act & Assess - expect(() => search(expression, data)).toThrow(error); - }); + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + } + ); it.each([ { @@ -188,7 +193,7 @@ describe('Slices tests', () => { expected: null, }, ])( - 'should support slicing an object with nested arrays with objects in them', + 'should support slicing an object with nested arrays with objects in them: $expression', ({ expression, expected }) => { // Prepare const data = { @@ -223,7 +228,7 @@ describe('Slices tests', () => { expected: [], }, ])( - 'should support slicing an array with objects in it', + 'should support slicing an array with objects in it: $expression', ({ expression, expected }) => { // Prepare const data = [{ a: 1 }, { a: 2 }, { a: 3 }]; From 5a7a4b77d91021419d3356869cdb473b754221c4 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 27 Sep 2023 18:41:50 +0000 Subject: [PATCH 060/103] chore: isMax --- .../actions/cached-node-modules/action.yml | 3 +- .github/scripts/release_patch_package_json.js | 4 +- ...sable-run-linting-check-and-unit-tests.yml | 4 +- packages/jmespath/package.json | 4 +- packages/jmespath/src/functions/Functions.ts | 68 ++- .../jmespath/src/functions/typeChecking.ts | 9 + .../jmespath/tests/unit/functions.test.ts | 540 +++++++++--------- packages/jmespath/tests/unit/index.test.ts | 22 + 8 files changed, 387 insertions(+), 267 deletions(-) create mode 100644 packages/jmespath/tests/unit/index.test.ts diff --git a/.github/actions/cached-node-modules/action.yml b/.github/actions/cached-node-modules/action.yml index 38d6e1e35c..bddc11a9dd 100644 --- a/.github/actions/cached-node-modules/action.yml +++ b/.github/actions/cached-node-modules/action.yml @@ -45,5 +45,6 @@ runs: npm run build -w packages/parameters & \ npm run build -w packages/idempotency & \ npm run build -w packages/batch & \ - npm run build -w packages/testing + npm run build -w packages/testing & \ + npm run build -w packages/jmespath shell: bash \ No newline at end of file diff --git a/.github/scripts/release_patch_package_json.js b/.github/scripts/release_patch_package_json.js index 761c0680a1..0f78d81bf3 100644 --- a/.github/scripts/release_patch_package_json.js +++ b/.github/scripts/release_patch_package_json.js @@ -18,7 +18,9 @@ if (process.argv.length < 3) { const basePath = resolve(process.argv[2]); const packageJsonPath = join(basePath, 'package.json'); const alphaPackages = []; -const betaPackages = []; +const betaPackages = [ + '@aws-lambda-powertools/jmespath', +]; (() => { try { diff --git a/.github/workflows/reusable-run-linting-check-and-unit-tests.yml b/.github/workflows/reusable-run-linting-check-and-unit-tests.yml index d7d7b2bd0a..1c669b18c5 100644 --- a/.github/workflows/reusable-run-linting-check-and-unit-tests.yml +++ b/.github/workflows/reusable-run-linting-check-and-unit-tests.yml @@ -28,9 +28,9 @@ jobs: with: nodeVersion: ${{ matrix.version }} - name: Run linting - run: npm run lint -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency -w packages/batch + run: npm run lint -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency -w packages/batch -w packages/jmespath - name: Run unit tests - run: npm t -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency -w packages/batch + run: npm t -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency -w packages/batch -w packages/jmespath check-examples: runs-on: ubuntu-latest env: diff --git a/packages/jmespath/package.json b/packages/jmespath/package.json index ade04f3d5a..93bc2d0cad 100644 --- a/packages/jmespath/package.json +++ b/packages/jmespath/package.json @@ -9,7 +9,7 @@ "private": true, "scripts": { "test": "npm run test:unit", - "test:unit": "jest --group=unit --detectOpenHandles --coverage --verbose", + "test:unit": "jest --group=unit --detectOpenHandles", "test:e2e": "echo 'Not applicable for this package'", "watch": "jest --watch", "build": "tsc", @@ -50,4 +50,4 @@ "serverless", "nodejs" ] -} +} \ No newline at end of file diff --git a/packages/jmespath/src/functions/Functions.ts b/packages/jmespath/src/functions/Functions.ts index cfe2c98eb7..50d38cca42 100644 --- a/packages/jmespath/src/functions/Functions.ts +++ b/packages/jmespath/src/functions/Functions.ts @@ -1,4 +1,4 @@ -import type { JSONArray, JSONValue } from '../types'; +import type { JSONArray, JSONObject, JSONValue } from '../types'; import { typeCheck, arityCheck } from './typeChecking'; /** @@ -96,6 +96,19 @@ class Functions { return Math.floor(args); } + /** + * Get the keys of the provided object. + * + * @param args The object to get the keys of + * @returns The keys of the object + */ + @Functions.signature({ + argumentsSpecs: [['object']], + }) + public funcKeys(arg: JSONObject): string[] { + return Object.keys(arg); + } + /** * Get the number of items in the provided item. * @@ -109,6 +122,46 @@ class Functions { return arg.length; } + /** + * Get the maximum value in the provided array. + * + * @param args The array to get the maximum value of + * @returns The maximum value in the array + */ + @Functions.signature({ + argumentsSpecs: [['array-number']], + }) + public funcMax(arg: Array): number { + return Math.max(...arg); + } + + /** + * Merge the provided objects into a single object. + * + * @param args The objects to merge + * @returns The merged object + */ + @Functions.signature({ + argumentsSpecs: [['object']], + variadic: true, + }) + public funcMerge(...args: Array): JSONObject { + return args.reduce((a, b) => ({ ...a, ...b }), {}); + } + + /** + * Get the minimum value in the provided array. + * + * @param args The array to get the minimum value of + * @returns The minimum value in the array + */ + @Functions.signature({ + argumentsSpecs: [['array-number']], + }) + public funcMin(arg: Array): number { + return Math.min(...arg); + } + /** * Get the first argument that does not evaluate to null. * If all arguments evaluate to null, then null is returned. @@ -203,6 +256,19 @@ class Functions { return typeof arg === 'string' ? arg : JSON.stringify(arg); } + /** + * Get the values of the provided object. + * + * @param args The object to get the values of + * @returns The values of the object + */ + @Functions.signature({ + argumentsSpecs: [['object']], + }) + public funcValues(arg: JSONObject): JSONValue[] { + return Object.values(arg); + } + /** * TODO: write docs for Functions.signature() * diff --git a/packages/jmespath/src/functions/typeChecking.ts b/packages/jmespath/src/functions/typeChecking.ts index c79302bb50..9bd5537146 100644 --- a/packages/jmespath/src/functions/typeChecking.ts +++ b/packages/jmespath/src/functions/typeChecking.ts @@ -1,3 +1,4 @@ +import { isRecord } from '../visitor/utils'; import { JMESPathTypeError, ArityError, VariadicArityError } from '../errors'; /** @@ -89,6 +90,14 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { actualType: typeof arg, }); } + } else if (type === 'object') { + if (!isRecord(arg)) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: typeof arg, + }); + } } } }); diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/functions.test.ts index 2dde2318b6..9ae2034e24 100644 --- a/packages/jmespath/tests/unit/functions.test.ts +++ b/packages/jmespath/tests/unit/functions.test.ts @@ -31,32 +31,35 @@ describe('Functions tests', () => { expression: 'abs(`-24`)', expected: 24, }, - ])('should support the abs() function', ({ expression, expected }) => { - // Prepare - const data = { - foo: -1, - zero: 0, - numbers: [-1, 3, 4, 5], - array: [-1, 3, 4, 5, 'a', '100'], - strings: ['a', 'b', 'c'], - decimals: [1.01, 1.2, -1.5], - str: 'Str', - false: false, - empty_list: [], - empty_hash: {}, - objects: { - foo: 'bar', - bar: 'baz', - }, - null_key: null, - }; + ])( + 'should support the abs() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -79,8 +82,7 @@ describe('Functions tests', () => { error: 'Expected at least 1 argument for function abs(), received 0 in expression: abs()', }, - ])('abs() function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in abs() fn errors tests + ])('abs() function errors: $expression', ({ expression, error }) => { // Prepare const data = { foo: -1, @@ -109,8 +111,7 @@ describe('Functions tests', () => { expression: 'unknown_function(`1`, `2`)', error: 'Unknown function: unknown_function()', }, - ])('unknown function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in unknown fn errors tests + ])('unknown function errors: $expression', ({ expression, error }) => { // Prepare const data = { foo: -1, @@ -139,32 +140,35 @@ describe('Functions tests', () => { expression: 'avg(numbers)', expected: 2.75, }, - ])('should support the avg() function', ({ expression, expected }) => { - // Prepare - const data = { - foo: -1, - zero: 0, - numbers: [-1, 3, 4, 5], - array: [-1, 3, 4, 5, 'a', '100'], - strings: ['a', 'b', 'c'], - decimals: [1.01, 1.2, -1.5], - str: 'Str', - false: false, - empty_list: [], - empty_hash: {}, - objects: { - foo: 'bar', - bar: 'baz', - }, - null_key: null, - }; + ])( + 'should support the avg() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -192,8 +196,7 @@ describe('Functions tests', () => { error: 'TypeError: avg() expected argument 1 to be type (Array) but received type array instead.', }, - ])('avg() function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in avg() fn errors tests + ])('avg() function errors: $expression', ({ expression, error }) => { // Prepare const data = { foo: -1, @@ -234,32 +237,35 @@ describe('Functions tests', () => { expression: 'ceil(decimals[2])', expected: -1, }, - ])('should support the ceil() function', ({ expression, expected }) => { - // Prepare - const data = { - foo: -1, - zero: 0, - numbers: [-1, 3, 4, 5], - array: [-1, 3, 4, 5, 'a', '100'], - strings: ['a', 'b', 'c'], - decimals: [1.01, 1.2, -1.5], - str: 'Str', - false: false, - empty_list: [], - empty_hash: {}, - objects: { - foo: 'bar', - bar: 'baz', - }, - null_key: null, - }; + ])( + 'should support the ceil() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -267,8 +273,7 @@ describe('Functions tests', () => { error: 'TypeError: ceil() expected argument 1 to be type (number) but received type string instead.', }, - ])('ceil() function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in ceil() fn errors tests + ])('ceil() function errors: $expression', ({ expression, error }) => { // Prepare const data = { foo: -1, @@ -313,32 +318,35 @@ describe('Functions tests', () => { expression: 'contains(decimals, `false`)', expected: false, }, - ])('should support the contains() function', ({ expression, expected }) => { - // Prepare - const data = { - foo: -1, - zero: 0, - numbers: [-1, 3, 4, 5], - array: [-1, 3, 4, 5, 'a', '100'], - strings: ['a', 'b', 'c'], - decimals: [1.01, 1.2, -1.5], - str: 'Str', - false: false, - empty_list: [], - empty_hash: {}, - objects: { - foo: 'bar', - bar: 'baz', - }, - null_key: null, - }; + ])( + 'should support the contains() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -346,8 +354,7 @@ describe('Functions tests', () => { error: 'TypeError: contains() expected argument 1 to be type (string | array) but received type boolean instead.', }, - ])('contains() function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in contains() fn errors tests + ])('contains() function errors: $expression', ({ expression, error }) => { // Prepare const data = { foo: -1, @@ -392,32 +399,35 @@ describe('Functions tests', () => { expression: `ends_with(str, 'foo')`, expected: false, }, - ])('should support the ends_with() function', ({ expression, expected }) => { - // Prepare - const data = { - foo: -1, - zero: 0, - numbers: [-1, 3, 4, 5], - array: [-1, 3, 4, 5, 'a', '100'], - strings: ['a', 'b', 'c'], - decimals: [1.01, 1.2, -1.5], - str: 'Str', - false: false, - empty_list: [], - empty_hash: {}, - objects: { - foo: 'bar', - bar: 'baz', - }, - null_key: null, - }; + ])( + 'should support the ends_with() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -425,8 +435,7 @@ describe('Functions tests', () => { error: 'TypeError: ends_with() expected argument 2 to be type (string) but received type number instead.', }, - ])('ends_with() function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in ends_with() fn errors tests + ])('ends_with() function errors: $expression', ({ expression, error }) => { // Prepare const data = { foo: -1, @@ -463,32 +472,35 @@ describe('Functions tests', () => { expression: 'floor(foo)', expected: -1, }, - ])('should support the floor() function', ({ expression, expected }) => { - // Prepare - const data = { - foo: -1, - zero: 0, - numbers: [-1, 3, 4, 5], - array: [-1, 3, 4, 5, 'a', '100'], - strings: ['a', 'b', 'c'], - decimals: [1.01, 1.2, -1.5], - str: 'Str', - false: false, - empty_list: [], - empty_hash: {}, - objects: { - foo: 'bar', - bar: 'baz', - }, - null_key: null, - }; + ])( + 'should support the floor() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -501,8 +513,7 @@ describe('Functions tests', () => { error: 'TypeError: floor() expected argument 1 to be type (number) but received type string instead.', }, - ])('floor() function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in floor() fn errors tests + ])('floor() function errors: $expression', ({ expression, error }) => { // Prepare const data = { foo: -1, @@ -563,32 +574,35 @@ describe('Functions tests', () => { expression: 'length(strings[0])', expected: 1, }, - ])('should support the length() function', ({ expression, expected }) => { - // Prepare - const data = { - foo: -1, - zero: 0, - numbers: [-1, 3, 4, 5], - array: [-1, 3, 4, 5, 'a', '100'], - strings: ['a', 'b', 'c'], - decimals: [1.01, 1.2, -1.5], - str: 'Str', - false: false, - empty_list: [], - empty_hash: {}, - objects: { - foo: 'bar', - bar: 'baz', - }, - null_key: null, - }; + ])( + 'should support the length() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -601,8 +615,7 @@ describe('Functions tests', () => { error: 'TypeError: length() expected argument 1 to be type (string | array | object) but received type number instead.', }, - ])('length() function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in length() fn errors tests + ])('length() function errors: $expression', ({ expression, error }) => { // Prepare const data = { foo: -1, @@ -647,32 +660,35 @@ describe('Functions tests', () => { expression: 'max(empty_list)', expected: null, }, - ])('should support the max() function', ({ expression, expected }) => { - // Prepare - const data = { - foo: -1, - zero: 0, - numbers: [-1, 3, 4, 5], - array: [-1, 3, 4, 5, 'a', '100'], - strings: ['a', 'b', 'c'], - decimals: [1.01, 1.2, -1.5], - str: 'Str', - false: false, - empty_list: [], - empty_hash: {}, - objects: { - foo: 'bar', - bar: 'baz', - }, - null_key: null, - }; + ])( + 'should support the max() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -685,8 +701,7 @@ describe('Functions tests', () => { error: 'TypeError: max() expected argument 1 to be type (Array | Array) but received type array instead.', }, - ])('max() function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in max() fn errors tests + ])('max() function errors: $expression', ({ expression, error }) => { // Prepare const data = { foo: -1, @@ -741,32 +756,35 @@ describe('Functions tests', () => { d: 4, }, }, - ])('should support the merge() function', ({ expression, expected }) => { - // Prepare - const data = { - foo: -1, - zero: 0, - numbers: [-1, 3, 4, 5], - array: [-1, 3, 4, 5, 'a', '100'], - strings: ['a', 'b', 'c'], - decimals: [1.01, 1.2, -1.5], - str: 'Str', - false: false, - empty_list: [], - empty_hash: {}, - objects: { - foo: 'bar', - bar: 'baz', - }, - null_key: null, - }; + ])( + 'should support the merge() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -789,32 +807,35 @@ describe('Functions tests', () => { expression: 'min(strings)', expected: 'a', }, - ])('should support the min() function', ({ expression, expected }) => { - // Prepare - const data = { - foo: -1, - zero: 0, - numbers: [-1, 3, 4, 5], - array: [-1, 3, 4, 5, 'a', '100'], - strings: ['a', 'b', 'c'], - decimals: [1.01, 1.2, -1.5], - str: 'Str', - false: false, - empty_list: [], - empty_hash: {}, - objects: { - foo: 'bar', - bar: 'baz', - }, - null_key: null, - }; + ])( + 'should support the min() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; - // Act - const result = search(expression, data); + // Act + const result = search(expression, data); - // Assess - expect(result).toStrictEqual(expected); - }); + // Assess + expect(result).toStrictEqual(expected); + } + ); it.each([ { @@ -827,8 +848,7 @@ describe('Functions tests', () => { error: 'TypeError: min() expected argument 1 to be type (Array | Array) but received type array instead.', }, - ])('min() function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in min() fn errors tests + ])('min() function errors: $expression', ({ expression, error }) => { // Prepare const data = { foo: -1, diff --git a/packages/jmespath/tests/unit/index.test.ts b/packages/jmespath/tests/unit/index.test.ts new file mode 100644 index 0000000000..12be4673f8 --- /dev/null +++ b/packages/jmespath/tests/unit/index.test.ts @@ -0,0 +1,22 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/canary + */ +import { search } from '../../src'; + +describe('index', () => { + it('should be defined', () => { + const expression = 'foo[8:2:0]'; + const data = { + type: 'object', + }; + + /* const res = search(expression, data); + expect(res).toEqual([1, 2]); */ + + expect(() => search(expression, data)).toThrowError( + 'Invalid slice, step cannot be 0' + ); + }); +}); From 9143b300a689099d6cc591e659339f23a7e791a7 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 5 Feb 2024 19:23:00 +0100 Subject: [PATCH 061/103] feat: min max and others --- packages/jmespath/package.json | 2 +- packages/jmespath/src/errors.ts | 4 +- packages/jmespath/src/functions/Functions.ts | 59 +++++-- .../jmespath/src/functions/typeChecking.ts | 68 +++++--- .../jmespath/src/visitor/TreeInterpreter.ts | 2 +- packages/jmespath/src/visitor/utils.ts | 8 +- .../jmespath/tests/unit/functions.test.ts | 163 ++++++++---------- 7 files changed, 177 insertions(+), 129 deletions(-) diff --git a/packages/jmespath/package.json b/packages/jmespath/package.json index 93bc2d0cad..85b4788f75 100644 --- a/packages/jmespath/package.json +++ b/packages/jmespath/package.json @@ -9,7 +9,7 @@ "private": true, "scripts": { "test": "npm run test:unit", - "test:unit": "jest --group=unit --detectOpenHandles", + "test:unit": "jest --group=unit/jmespath --detectOpenHandles", "test:e2e": "echo 'Not applicable for this package'", "watch": "jest --watch", "build": "tsc", diff --git a/packages/jmespath/src/errors.ts b/packages/jmespath/src/errors.ts index cec250867a..9bb97ad198 100644 --- a/packages/jmespath/src/errors.ts +++ b/packages/jmespath/src/errors.ts @@ -270,12 +270,12 @@ class JMESPathTypeError extends FunctionError { * provided by the user. If the function name is not found, this error is thrown. */ class UnknownFunctionError extends FunctionError { - public constructor() { + public constructor(funcName: string) { super('Unknown function'); this.name = 'UnknownFunctionError'; // Set the message to include the error info. - this.message = `Unknown function:`; + this.message = `Unknown function: ${funcName}()`; } } diff --git a/packages/jmespath/src/functions/Functions.ts b/packages/jmespath/src/functions/Functions.ts index 50d38cca42..fb3e28ce5d 100644 --- a/packages/jmespath/src/functions/Functions.ts +++ b/packages/jmespath/src/functions/Functions.ts @@ -1,3 +1,4 @@ +import { isNumber, isRecord } from '../visitor/utils'; import type { JSONArray, JSONObject, JSONValue } from '../types'; import { typeCheck, arityCheck } from './typeChecking'; @@ -68,7 +69,6 @@ class Functions { argumentsSpecs: [['array', 'string'], ['any']], }) public funcContains(haystack: string, needle: string): boolean { - // TODO: review this implementation return haystack.includes(needle); } @@ -116,10 +116,16 @@ class Functions { * @returns The length of the array */ @Functions.signature({ - argumentsSpecs: [['array', 'string']], + argumentsSpecs: [['array', 'string', 'object']], }) - public funcLength(arg: string | Array): number { - return arg.length; + public funcLength( + arg: string | Array | Record + ): number { + if (isRecord(arg)) { + return Object.keys(arg).length; + } else { + return arg.length; + } } /** @@ -129,10 +135,18 @@ class Functions { * @returns The maximum value in the array */ @Functions.signature({ - argumentsSpecs: [['array-number']], + argumentsSpecs: [['array-number', 'array-string']], }) - public funcMax(arg: Array): number { - return Math.max(...arg); + public funcMax(arg: Array): number | string | null { + if (arg.length === 0) { + return null; + // The signature decorator already enforces that all elements are of the same type + } else if (isNumber(arg[0])) { + return Math.max(...(arg as number[])); + } else { + // local compare function to handle string comparison + return arg.reduce((a, b) => (a > b ? a : b)); + } } /** @@ -156,10 +170,17 @@ class Functions { * @returns The minimum value in the array */ @Functions.signature({ - argumentsSpecs: [['array-number']], + argumentsSpecs: [['array-number', 'array-string']], }) - public funcMin(arg: Array): number { - return Math.min(...arg); + public funcMin(arg: Array): number | string | null { + if (arg.length === 0) { + return null; + // The signature decorator already enforces that all elements are of the same type + } else if (isNumber(arg[0])) { + return Math.min(...arg); + } else { + return arg.reduce((a, b) => (a < b ? a : b)); + } } /** @@ -256,6 +277,24 @@ class Functions { return typeof arg === 'string' ? arg : JSON.stringify(arg); } + public funcType(arg: Array): string { + if (Array.isArray(arg[0])) { + return 'array'; + } else if (isRecord(arg[0])) { + return 'object'; + } else if (typeof arg[0] === 'string') { + return 'string'; + } else if (typeof arg[0] === 'number') { + return 'number'; + } else if (typeof arg[0] === 'boolean') { + return 'boolean'; + } else if (Object.is(arg[0], null)) { + return 'null'; + } else { + return 'unknown'; + } + } + /** * Get the values of the provided object. * diff --git a/packages/jmespath/src/functions/typeChecking.ts b/packages/jmespath/src/functions/typeChecking.ts index 9bd5537146..bc642962fd 100644 --- a/packages/jmespath/src/functions/typeChecking.ts +++ b/packages/jmespath/src/functions/typeChecking.ts @@ -54,13 +54,18 @@ const typeCheck = ( * @param argumentSpec */ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { - // TODO: check if all types in argumentSpec are valid if (argumentSpec.length === 0 || argumentSpec[0] === 'any') { return; } - argumentSpec.forEach((type) => { + const entryCount = argumentSpec.length; + let hasMoreTypesToCheck = argumentSpec.length > 1; + for (const [index, type] of argumentSpec.entries()) { + hasMoreTypesToCheck = index < entryCount - 1; if (type.startsWith('array')) { if (!Array.isArray(arg)) { + if (hasMoreTypesToCheck) { + continue; + } throw new JMESPathTypeError({ currentValue: arg, expectedTypes: argumentSpec, @@ -69,38 +74,59 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { } if (type.includes('-')) { const arrayItemsType = type.slice(6); - arg.forEach((element) => { - typeCheckArgument(element, [arrayItemsType]); - }); + let actualType: string | undefined; + for (const element of arg) { + try { + typeCheckArgument(element, [arrayItemsType]); + actualType = arrayItemsType; + } catch (error) { + if (!hasMoreTypesToCheck || actualType !== undefined) { + throw error; + } + } + } } + break; } else { if (type === 'string' || type === 'number' || type === 'boolean') { if (typeof arg !== type) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: type === 'boolean' ? 'boolean' : typeof arg, // TODO: fix this - }); + if (!hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: type === 'boolean' ? 'boolean' : typeof arg, // TODO: fix this + }); + } + continue; } + break; } else if (type === 'null') { if (!Object.is(arg, null)) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: typeof arg, - }); + if (!hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: typeof arg, + }); + } + continue; } + break; } else if (type === 'object') { if (!isRecord(arg)) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: typeof arg, - }); + if (index === entryCount - 1) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: typeof arg, + }); + } + continue; } + break; } } - }); + } }; export { arityCheck, typeCheck }; diff --git a/packages/jmespath/src/visitor/TreeInterpreter.ts b/packages/jmespath/src/visitor/TreeInterpreter.ts index 41c33e592c..ab58766ae5 100644 --- a/packages/jmespath/src/visitor/TreeInterpreter.ts +++ b/packages/jmespath/src/visitor/TreeInterpreter.ts @@ -215,7 +215,7 @@ class TreeInterpreter { }`; const methodName = methods.find((method) => method === funcName); if (!methodName) { - throw new UnknownFunctionError(); + throw new UnknownFunctionError(node.value); } try { diff --git a/packages/jmespath/src/visitor/utils.ts b/packages/jmespath/src/visitor/utils.ts index da5059020b..acc8d7f092 100644 --- a/packages/jmespath/src/visitor/utils.ts +++ b/packages/jmespath/src/visitor/utils.ts @@ -183,14 +183,14 @@ const sliceArray = ( start = isIntegerNumber(start) ? capSliceRange(length, start, isStepNegative) : isStepNegative - ? length - 1 - : 0; + ? length - 1 + : 0; end = isIntegerNumber(end) ? capSliceRange(length, end, isStepNegative) : isStepNegative - ? -1 - : length; + ? -1 + : length; const result: T[] = []; if (step > 0) { diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/functions.test.ts index 9ae2034e24..8750c5caea 100644 --- a/packages/jmespath/tests/unit/functions.test.ts +++ b/packages/jmespath/tests/unit/functions.test.ts @@ -109,7 +109,8 @@ describe('Functions tests', () => { it.each([ { expression: 'unknown_function(`1`, `2`)', - error: 'Unknown function: unknown_function()', + error: + 'Unknown function: unknown_function() in expression: unknown_function(`1`, `2`)', }, ])('unknown function errors: $expression', ({ expression, error }) => { // Prepare @@ -174,27 +175,26 @@ describe('Functions tests', () => { { expression: 'avg(array)', error: - 'TypeError: avg() expected argument 1 to be type (Array) but received type array instead.', + 'Invalid argument type for function avg(), expected "number" but found "string" in expression: avg(array)', }, { expression: `avg('abc')`, - error: - 'TypeError: avg() expected argument 1 to be type (Array) but received type string instead.', + error: `Invalid argument type for function avg(), expected "array-number" but found "string" in expression: avg('abc')`, }, { expression: 'avg(foo)', error: - 'TypeError: avg() expected argument 1 to be type (Array) but received type number instead.', + 'Invalid argument type for function avg(), expected "array-number" but found "number" in expression: avg(foo)', }, { expression: 'avg(@)', error: - 'TypeError: avg() expected argument 1 to be type (Array) but received type object instead.', + 'Invalid argument type for function avg(), expected "array-number" but found "object" in expression: avg(@)', }, { expression: 'avg(strings)', error: - 'TypeError: avg() expected argument 1 to be type (Array) but received type array instead.', + 'Invalid argument type for function avg(), expected "number" but found "string" in expression: avg(strings)', }, ])('avg() function errors: $expression', ({ expression, error }) => { // Prepare @@ -270,8 +270,7 @@ describe('Functions tests', () => { it.each([ { expression: `ceil('string')`, - error: - 'TypeError: ceil() expected argument 1 to be type (number) but received type string instead.', + error: `Invalid argument type for function ceil(), expected "number" but found "string" in expression: ceil('string')`, }, ])('ceil() function errors: $expression', ({ expression, error }) => { // Prepare @@ -299,15 +298,16 @@ describe('Functions tests', () => { it.each([ { - expression: 'contains("abc", "a")', + expression: `contains('abc', 'a')`, expected: true, }, { - expression: 'contains("abc", "d")', + expression: `contains('abc', 'd')`, expected: false, }, { - expression: 'contains(strings, "a")', + // prettier-ignore + expression: 'contains(strings, \'a\')', expected: true, }, { @@ -352,7 +352,7 @@ describe('Functions tests', () => { { expression: 'contains(`false`, "d")', error: - 'TypeError: contains() expected argument 1 to be type (string | array) but received type boolean instead.', + 'Invalid argument type for function contains(), expected one of "array", "string" but found "boolean" in expression: contains(`false`, "d")', }, ])('contains() function errors: $expression', ({ expression, error }) => { // Prepare @@ -433,7 +433,7 @@ describe('Functions tests', () => { { expression: 'ends_with(str, `0`)', error: - 'TypeError: ends_with() expected argument 2 to be type (string) but received type number instead.', + 'Invalid argument type for function ends_with(), expected "string" but found "number" in expression: ends_with(str, `0`)', }, ])('ends_with() function errors: $expression', ({ expression, error }) => { // Prepare @@ -505,13 +505,12 @@ describe('Functions tests', () => { it.each([ { expression: `floor('string')`, - error: - 'TypeError: floor() expected argument 1 to be type (number) but received type string instead.', + error: `Invalid argument type for function floor(), expected "number" but found "string" in expression: floor('string')`, }, { expression: 'floor(str)', error: - 'TypeError: floor() expected argument 1 to be type (number) but received type string instead.', + 'Invalid argument type for function floor(), expected "number" but found "string" in expression: floor(str)', }, ])('floor() function errors: $expression', ({ expression, error }) => { // Prepare @@ -608,12 +607,12 @@ describe('Functions tests', () => { { expression: 'length(`false`)', error: - 'TypeError: length() expected argument 1 to be type (string | array | object) but received type boolean instead.', + 'Invalid argument type for function length(), expected one of "array", "string", "object" but found "boolean" in expression: length(`false`)', }, { expression: 'length(foo)', error: - 'TypeError: length() expected argument 1 to be type (string | array | object) but received type number instead.', + 'Invalid argument type for function length(), expected one of "array", "string", "object" but found "number" in expression: length(foo)', }, ])('length() function errors: $expression', ({ expression, error }) => { // Prepare @@ -694,12 +693,12 @@ describe('Functions tests', () => { { expression: 'max(abc)', error: - 'TypeError: max() expected argument 1 to be type (Array | Array) but received type null instead.', + 'Invalid argument type for function max(), expected one of "array-number", "array-string" but found "object" in expression: max(abc)', }, { expression: 'max(array)', error: - 'TypeError: max() expected argument 1 to be type (Array | Array) but received type array instead.', + 'Invalid argument type for function max(), expected "number" but found "string" in expression: max(array)', }, ])('max() function errors: $expression', ({ expression, error }) => { // Prepare @@ -725,7 +724,7 @@ describe('Functions tests', () => { expect(() => search(expression, data)).toThrow(error); }); - it.each([ + /* it.each([ { expression: 'merge(`{}`)', expected: {}, @@ -784,7 +783,7 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); } - ); + ); */ it.each([ { @@ -841,12 +840,12 @@ describe('Functions tests', () => { { expression: 'min(abc)', error: - 'TypeError: min() expected argument 1 to be type (Array | Array) but received type null instead.', + 'Invalid argument type for function min(), expected one of "array-number", "array-string" but found "object" in expression: min(abc)', }, { expression: 'min(array)', error: - 'TypeError: min() expected argument 1 to be type (Array | Array) but received type array instead.', + 'Invalid argument type for function min(), expected "number" but found "string" in expression: min(array)', }, ])('min() function errors: $expression', ({ expression, error }) => { // Prepare @@ -936,7 +935,7 @@ describe('Functions tests', () => { expect(result).toStrictEqual(expected); }); - it.each([ + /* it.each([ { expression: 'sort(keys(objects))', expected: ['bar', 'foo'], @@ -990,9 +989,9 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); } - ); + ); */ - it.each([ + /* it.each([ { expression: 'keys(foo)', error: @@ -1044,9 +1043,9 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); } - ); + ); */ - it.each([ + /* it.each([ { expression: `join(', ', strings)`, expected: 'a, b, c', @@ -1096,9 +1095,9 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); + }); */ - it.each([ + /* it.each([ { expression: 'join(\',\', `["a", 0]`)', error: @@ -1128,9 +1127,9 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); - }); + }); */ - it.each([ + /* it.each([ { expression: 'reverse(numbers)', expected: [5, 4, 3, -1], @@ -1176,7 +1175,7 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); + }); */ it.each([ { @@ -1229,10 +1228,9 @@ describe('Functions tests', () => { { expression: 'starts_with(str, `0`)', error: - 'TypeError: starts_with() expected argument 2 to be type (string) but received type number instead.', + 'Invalid argument type for function starts_with(), expected "string" but found "object" in expression: starts_with(str, `0`)', }, ])('starts_with() function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in starts_with() errors tests // Prepare const data = { type: 'object', @@ -1242,7 +1240,7 @@ describe('Functions tests', () => { expect(() => search(expression, data)).toThrow(error); }); - it.each([ + /* it.each([ { expression: 'sum(numbers)', expected: 11, @@ -1301,9 +1299,9 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); - }); + }); */ - it.each([ + /* it.each([ { expression: `to_array('foo')`, expected: ['foo'], @@ -1354,9 +1352,9 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); + }); */ - it.each([ + /* it.each([ { expression: `to_string('foo')`, expected: 'foo', @@ -1399,9 +1397,8 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); - - it.each([ + }); */ + /* it.each([ { expression: `to_number('1.0')`, expected: 1.0, @@ -1464,9 +1461,8 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); - - it.each([ + }); */ + /* it.each([ { expression: '"to_string"(`1.0`)', error: 'Quoted identifier not allowed for function names.', @@ -1480,9 +1476,8 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); - }); - - it.each([ + }); */ + /* it.each([ { expression: 'not_null(unknown_key, str)', expected: 'Str', @@ -1524,9 +1519,8 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); - - it.each([ + }); */ + /* it.each([ { expression: 'not_null()', error: @@ -1541,9 +1535,8 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); - }); - - it.each([ + }); */ + /* it.each([ { description: 'function projection on variadic function', expression: 'foo[].not_null(f, e, d, c, b, a)', @@ -1584,9 +1577,8 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); } - ); - - it.each([ + ); */ + /* it.each([ { description: 'sort by field expression', expression: 'sort_by(people, &age)', @@ -1758,9 +1750,8 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); - - it.each([ + }); */ + /* it.each([ { expression: 'sort_by(people, &extra)', error: 'TypeError: expected (string), received null', @@ -1820,9 +1811,8 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); - }); - - it.each([ + }); */ + /* it.each([ { expression: 'max_by(people, &age)', expected: { @@ -1894,9 +1884,8 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); - - it.each([ + }); */ + /* it.each([ { expression: 'max_by(people, &bool)', error: 'TypeError: expected one of (number | string), received boolean', @@ -1947,9 +1936,8 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); - }); - - it.each([ + }); */ + /* it.each([ { expression: 'min_by(people, &age)', expected: { @@ -2021,9 +2009,8 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); - - it.each([ + }); */ + /* it.each([ { expression: 'min_by(people, &bool)', error: 'TypeError: expected one of (number | string), received boolean', @@ -2075,8 +2062,8 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); }); - - it.each([ + */ + /* it.each([ { description: 'stable sort order', expression: 'sort_by(people, &age)', @@ -2183,9 +2170,8 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); - - it.each([ + }); */ + /* it.each([ { expression: 'map(&a, people)', expected: [10, 10, 10, 10, 10, 10, 10, 10, 10], @@ -2249,9 +2235,8 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); - - it.each([ + }); */ + /* it.each([ { expression: 'map(&a, badkey)', error: @@ -2314,9 +2299,8 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); - }); - - it.each([ + }); */ + /* it.each([ { expression: 'map(&foo.bar, array)', expected: ['yes1', 'yes2', null], @@ -2359,9 +2343,8 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); } - ); - - it.each([ + ); */ + /* it.each([ { expression: 'map(&[], array)', expected: [ @@ -2383,5 +2366,5 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); + }); */ }); From 0fb71086e214a41b2b63cb5e2891d5ebd374be21 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 6 Feb 2024 13:49:29 +0100 Subject: [PATCH 062/103] feat: map function --- packages/jmespath/src/functions/Functions.ts | 17 ++++++++- .../jmespath/src/functions/typeChecking.ts | 2 +- packages/jmespath/src/visitor/utils.ts | 7 ++-- .../jmespath/tests/unit/functions.test.ts | 35 ++++++++++--------- 4 files changed, 39 insertions(+), 22 deletions(-) diff --git a/packages/jmespath/src/functions/Functions.ts b/packages/jmespath/src/functions/Functions.ts index fb3e28ce5d..a985547832 100644 --- a/packages/jmespath/src/functions/Functions.ts +++ b/packages/jmespath/src/functions/Functions.ts @@ -1,4 +1,4 @@ -import { isNumber, isRecord } from '../visitor/utils'; +import { Expression, isNumber, isRecord } from '../visitor/utils'; import type { JSONArray, JSONObject, JSONValue } from '../types'; import { typeCheck, arityCheck } from './typeChecking'; @@ -128,6 +128,15 @@ class Functions { } } + @Functions.signature({ + argumentsSpecs: [['any'], ['array']], + }) + public funcMap(expression: Expression, args: JSONArray): JSONArray { + return args.map((arg: JSONValue) => { + return expression.visit(arg) || null; + }); + } + /** * Get the maximum value in the provided array. * @@ -277,6 +286,12 @@ class Functions { return typeof arg === 'string' ? arg : JSON.stringify(arg); } + /** + * Get the type of the provided value. + * + * @param arg The value to check the type of + * @returns The type of the value + */ public funcType(arg: Array): string { if (Array.isArray(arg[0])) { return 'array'; diff --git a/packages/jmespath/src/functions/typeChecking.ts b/packages/jmespath/src/functions/typeChecking.ts index bc642962fd..3dd0c65cbf 100644 --- a/packages/jmespath/src/functions/typeChecking.ts +++ b/packages/jmespath/src/functions/typeChecking.ts @@ -69,7 +69,7 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { throw new JMESPathTypeError({ currentValue: arg, expectedTypes: argumentSpec, - actualType: typeof arg, + actualType: Object.is(arg, null) ? 'null' : typeof arg, }); } if (type.includes('-')) { diff --git a/packages/jmespath/src/visitor/utils.ts b/packages/jmespath/src/visitor/utils.ts index acc8d7f092..3df8e15142 100644 --- a/packages/jmespath/src/visitor/utils.ts +++ b/packages/jmespath/src/visitor/utils.ts @@ -2,8 +2,7 @@ import type { TreeInterpreter } from './TreeInterpreter'; import type { Node, JSONValue } from '../types'; /** - * TODO: write docs for Expression - * TODO: see if #expression is needed + * Apply a JMESPath expression to a JSON value. */ class Expression { readonly #expression: Node; @@ -14,8 +13,8 @@ class Expression { this.#interpreter = interpreter; } - public visit(node: Node, value: JSONValue): JSONValue { - return this.#interpreter.visit(node, value); + public visit(value: JSONValue, node?: Node): JSONValue { + return this.#interpreter.visit(node ?? this.#expression, value); } } diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/functions.test.ts index 8750c5caea..68a21b2d06 100644 --- a/packages/jmespath/tests/unit/functions.test.ts +++ b/packages/jmespath/tests/unit/functions.test.ts @@ -693,7 +693,7 @@ describe('Functions tests', () => { { expression: 'max(abc)', error: - 'Invalid argument type for function max(), expected one of "array-number", "array-string" but found "object" in expression: max(abc)', + 'Invalid argument type for function max(), expected one of "array-number", "array-string" but found "null" in expression: max(abc)', }, { expression: 'max(array)', @@ -840,7 +840,7 @@ describe('Functions tests', () => { { expression: 'min(abc)', error: - 'Invalid argument type for function min(), expected one of "array-number", "array-string" but found "object" in expression: min(abc)', + 'Invalid argument type for function min(), expected one of "array-number", "array-string" but found "null" in expression: min(abc)', }, { expression: 'min(array)', @@ -2171,7 +2171,8 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); }); */ - /* it.each([ + + it.each([ { expression: 'map(&a, people)', expected: [10, 10, 10, 10, 10, 10, 10, 10, 10], @@ -2180,6 +2181,10 @@ describe('Functions tests', () => { expression: 'map(&c, people)', expected: ['z', null, null, 'z', null, null, 'z', null, null], }, + { + expression: 'map(&foo, empty)', + expected: [], + }, ])('should support map() special cases', ({ expression, expected }) => { // Prepare const data = { @@ -2235,19 +2240,15 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); */ - /* it.each([ + }); + + it.each([ { expression: 'map(&a, badkey)', error: - 'TypeError: map() expected argument 2 to be type (array) but received type null instead.', - }, - { - expression: 'map(&foo, empty)', - expected: [], + 'Invalid argument type for function map(), expected "array" but found "null" in expression: map(&a, badkey)', }, ])('map() function special cases errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in map() function special cases errors tests // Prepare const data = { people: [ @@ -2299,8 +2300,9 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); - }); */ - /* it.each([ + }); + + it.each([ { expression: 'map(&foo.bar, array)', expected: ['yes1', 'yes2', null], @@ -2343,8 +2345,9 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); } - ); */ - /* it.each([ + ); + + it.each([ { expression: 'map(&[], array)', expected: [ @@ -2366,5 +2369,5 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); */ + }); }); From 323e057c51c4c91ba0032912e0141ea5575f9a4e Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 6 Feb 2024 14:44:01 +0100 Subject: [PATCH 063/103] feat: toString, toArray, & toNumber functions --- packages/jmespath/src/Parser.ts | 16 +----------- packages/jmespath/src/errors.ts | 5 ++-- packages/jmespath/src/functions/Functions.ts | 26 ++++++++++++++----- .../jmespath/tests/unit/functions.test.ts | 23 +++++++++------- 4 files changed, 36 insertions(+), 34 deletions(-) diff --git a/packages/jmespath/src/Parser.ts b/packages/jmespath/src/Parser.ts index aa33a9df98..1dde79c501 100644 --- a/packages/jmespath/src/Parser.ts +++ b/packages/jmespath/src/Parser.ts @@ -176,7 +176,7 @@ class Parser { lexPosition: 0, tokenValue: token.value, tokenType: token.type, - reason: 'Quoted identifier cannot be used as a function name', + reason: 'quoted identifiers cannot be used as a function name', }); } @@ -247,7 +247,6 @@ class Parser { lexPosition: token.start, tokenValue: token.value, tokenType: token.type, - reason: 'invalid token', }); } } @@ -354,7 +353,6 @@ class Parser { lexPosition: token.start, tokenValue: token.value, tokenType: token.type, - reason: 'invalid token', }); } } @@ -402,7 +400,6 @@ class Parser { lexPosition: token.start, tokenValue: token.value, tokenType: token.type, - reason: 'syntax error', }); } this.#advance(); @@ -419,7 +416,6 @@ class Parser { lexPosition: token.start, tokenValue: token.value, tokenType: token.type, - reason: 'syntax error', }); } currentToken = this.#currentToken(); @@ -531,7 +527,6 @@ class Parser { lexPosition: token.start, tokenValue: token.value, tokenType: token.type, - reason: 'syntax error', }); } @@ -568,17 +563,10 @@ class Parser { return this.#parseMultiSelectHash(); } else { const token = this.#lookaheadToken(0); - const allowed = [ - 'quoted_identifier', - 'unquoted_identifier', - 'lbracket', - 'lbrace', - ]; throw new ParseError({ lexPosition: token.start, tokenValue: token.value, tokenType: token.type, - reason: `Expecting one of: ${allowed.join(', ')}, got: ${token.type}`, }); } } @@ -605,7 +593,6 @@ class Parser { lexPosition: token.start, tokenValue: token.value, tokenType: token.type, - reason: `Expecting: ${tokenType}, got: ${token.type}`, }); } } @@ -631,7 +618,6 @@ class Parser { lexPosition: token.start, tokenValue: token.value, tokenType: token.type, - reason: `Expecting: ${tokenTypes}, got: ${token.type}`, }); } } diff --git a/packages/jmespath/src/errors.ts b/packages/jmespath/src/errors.ts index 9bb97ad198..0f878cd6a1 100644 --- a/packages/jmespath/src/errors.ts +++ b/packages/jmespath/src/errors.ts @@ -94,8 +94,9 @@ class ParseError extends JMESPathError { this.reason = options.reason; // Set the message to include the lexer position and token info. - const issue = - this.tokenType === 'eof' + const issue = this.reason + ? this.reason + : this.tokenType === 'eof' ? 'found unexpected end of expression (EOF)' : `found unexpected token "${this.tokenValue}" (${this.tokenType})`; this.message = `${this.message}: parse error at column ${this.lexPosition}, ${issue}`; diff --git a/packages/jmespath/src/functions/Functions.ts b/packages/jmespath/src/functions/Functions.ts index a985547832..4973895555 100644 --- a/packages/jmespath/src/functions/Functions.ts +++ b/packages/jmespath/src/functions/Functions.ts @@ -244,6 +244,9 @@ class Functions { * @param args The items to convert to an array * @returns The items as an array */ + @Functions.signature({ + argumentsSpecs: [['any']], + }) public funcToArray( arg: JSONArray | Array ): Array | JSONArray { @@ -261,6 +264,9 @@ class Functions { * @param arg The value to convert to a number * @returns The value as a number or null if the value cannot be converted to a number */ + @Functions.signature({ + argumentsSpecs: [['any']], + }) public funcToNumber(arg: JSONValue): number | null { if (typeof arg === 'number') { return arg; @@ -282,6 +288,9 @@ class Functions { * @param arg The value to convert to a string * @returns The value as a string */ + @Functions.signature({ + argumentsSpecs: [['any']], + }) public funcToString(arg: JSONValue): string { return typeof arg === 'string' ? arg : JSON.stringify(arg); } @@ -292,18 +301,21 @@ class Functions { * @param arg The value to check the type of * @returns The type of the value */ - public funcType(arg: Array): string { - if (Array.isArray(arg[0])) { + @Functions.signature({ + argumentsSpecs: [['any']], + }) + public funcType(arg: JSONValue): string { + if (Array.isArray(arg)) { return 'array'; - } else if (isRecord(arg[0])) { + } else if (isRecord(arg)) { return 'object'; - } else if (typeof arg[0] === 'string') { + } else if (typeof arg === 'string') { return 'string'; - } else if (typeof arg[0] === 'number') { + } else if (typeof arg === 'number') { return 'number'; - } else if (typeof arg[0] === 'boolean') { + } else if (typeof arg === 'boolean') { return 'boolean'; - } else if (Object.is(arg[0], null)) { + } else if (Object.is(arg, null)) { return 'null'; } else { return 'unknown'; diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/functions.test.ts index 68a21b2d06..afa746f30a 100644 --- a/packages/jmespath/tests/unit/functions.test.ts +++ b/packages/jmespath/tests/unit/functions.test.ts @@ -1301,7 +1301,7 @@ describe('Functions tests', () => { expect(() => search(expression, data)).toThrow(error); }); */ - /* it.each([ + it.each([ { expression: `to_array('foo')`, expected: ['foo'], @@ -1352,9 +1352,9 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); */ + }); - /* it.each([ + it.each([ { expression: `to_string('foo')`, expected: 'foo', @@ -1397,8 +1397,9 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); */ - /* it.each([ + }); + + it.each([ { expression: `to_number('1.0')`, expected: 1.0, @@ -1461,14 +1462,15 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); */ - /* it.each([ + }); + + it.each([ { expression: '"to_string"(`1.0`)', - error: 'Quoted identifier not allowed for function names.', + error: + 'Invalid jmespath expression: parse error at column 0, quoted identifiers cannot be used as a function name in expression: "to_string"(`1.0`)', }, ])('to_number() function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in to_number() errors tests // Prepare const data = { type: 'object', @@ -1476,7 +1478,8 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); - }); */ + }); + /* it.each([ { expression: 'not_null(unknown_key, str)', From 12175e61b76e79585f5542f20e4825f411e106e2 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 6 Feb 2024 14:51:50 +0100 Subject: [PATCH 064/103] feat: sum function --- packages/jmespath/src/functions/Functions.ts | 13 +++++++++++++ packages/jmespath/tests/unit/functions.test.ts | 9 +++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/jmespath/src/functions/Functions.ts b/packages/jmespath/src/functions/Functions.ts index 4973895555..f942489521 100644 --- a/packages/jmespath/src/functions/Functions.ts +++ b/packages/jmespath/src/functions/Functions.ts @@ -235,6 +235,19 @@ class Functions { return str.startsWith(suffix); } + /** + * Sum the provided numbers. + * + * @param args The numbers to sum + * @returns The sum of the numbers + */ + @Functions.signature({ + argumentsSpecs: [['array-number']], + }) + public funcSum(args: Array): number { + return args.reduce((a, b) => a + b, 0); + } + /** * Convert the provided value to an array. * diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/functions.test.ts index afa746f30a..7ae7c4c384 100644 --- a/packages/jmespath/tests/unit/functions.test.ts +++ b/packages/jmespath/tests/unit/functions.test.ts @@ -1240,7 +1240,7 @@ describe('Functions tests', () => { expect(() => search(expression, data)).toThrow(error); }); - /* it.each([ + it.each([ { expression: 'sum(numbers)', expected: 11, @@ -1288,10 +1288,9 @@ describe('Functions tests', () => { { expression: 'sum(array)', error: - 'TypeError: sum() expected argument 1 to be type (Array) but received type array instead.', + 'Invalid argument type for function sum(), expected "array-number" but found "null" in expression: sum(array)', }, ])('sum() function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in sum() errors tests // Prepare const data = { type: 'object', @@ -1299,7 +1298,7 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); - }); */ + }); it.each([ { @@ -1539,6 +1538,7 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); }); */ + /* it.each([ { description: 'function projection on variadic function', @@ -1581,6 +1581,7 @@ describe('Functions tests', () => { expect(result).toStrictEqual(expected); } ); */ + /* it.each([ { description: 'sort by field expression', From fec4af95333c5700079298061a38d43614b43a25 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 6 Feb 2024 15:41:12 +0100 Subject: [PATCH 065/103] feat: notNull function --- packages/jmespath/src/functions/Functions.ts | 16 +++++++++++++--- packages/jmespath/src/functions/typeChecking.ts | 11 ++++++++++- packages/jmespath/tests/unit/functions.test.ts | 16 ++++++++-------- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/jmespath/src/functions/Functions.ts b/packages/jmespath/src/functions/Functions.ts index f942489521..4385d8fbc9 100644 --- a/packages/jmespath/src/functions/Functions.ts +++ b/packages/jmespath/src/functions/Functions.ts @@ -62,8 +62,11 @@ class Functions { } /** - * Determine if the provided value is contained in the provided item. - * TODO: write docs for funcContains() + * Determine if the given value is contained in the provided array or string. + * + * @param haystack The array or string to check + * @param needle The value to check for + * @returns True if the value is in the array or string, false otherwise */ @Functions.signature({ argumentsSpecs: [['array', 'string'], ['any']], @@ -128,6 +131,13 @@ class Functions { } } + /** + * Map the provided function over the provided array. + * + * @param expression The expression to map over the array + * @param args The array to map the expression over + * @returns The result of mapping the expression over the array + */ @Functions.signature({ argumentsSpecs: [['any'], ['array']], }) @@ -203,7 +213,7 @@ class Functions { argumentsSpecs: [[]], variadic: true, }) - public funcNotNull(args: Array): JSONValue | null { + public funcNotNull(...args: Array): JSONValue | null { return args.find((arg) => !Object.is(arg, null)) || null; } diff --git a/packages/jmespath/src/functions/typeChecking.ts b/packages/jmespath/src/functions/typeChecking.ts index 3dd0c65cbf..c09c3b6fcb 100644 --- a/packages/jmespath/src/functions/typeChecking.ts +++ b/packages/jmespath/src/functions/typeChecking.ts @@ -44,12 +44,21 @@ const typeCheck = ( }; /** - * TODO: write docs for Functions.#typeCheckArgument() + * Type checks an argument against a list of types. * * Type checking at runtime involves checking the top level type, * and in the case of arrays, potentially checking the types of * the elements in the array. * + * If the list of types includes 'any', then the type check is a + * no-op. + * + * If the list of types includes more than one type, then the + * argument is checked against each type in the list. If the + * argument matches any of the types, then the type check + * passes. If the argument does not match any of the types, then + * a JMESPathTypeError is thrown. + * * @param arg * @param argumentSpec */ diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/functions.test.ts index 7ae7c4c384..198c9addbb 100644 --- a/packages/jmespath/tests/unit/functions.test.ts +++ b/packages/jmespath/tests/unit/functions.test.ts @@ -1479,7 +1479,7 @@ describe('Functions tests', () => { expect(() => search(expression, data)).toThrow(error); }); - /* it.each([ + it.each([ { expression: 'not_null(unknown_key, str)', expected: 'Str', @@ -1521,15 +1521,15 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); */ - /* it.each([ + }); + + it.each([ { expression: 'not_null()', error: - 'ArgumentError: not_null() takes at least 1 argument but received 0', + 'Expected 1 argument for function not_null(), received 0 in expression: not_null()', }, ])('not_null() function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in not_null() errors tests // Prepare const data = { type: 'object', @@ -1537,9 +1537,9 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); - }); */ + }); - /* it.each([ + it.each([ { description: 'function projection on variadic function', expression: 'foo[].not_null(f, e, d, c, b, a)', @@ -1580,7 +1580,7 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); } - ); */ + ); /* it.each([ { From 915675240d33392e2ac66406faae1efe0f03597c Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 6 Feb 2024 15:49:56 +0100 Subject: [PATCH 066/103] feat: merge function --- packages/jmespath/src/functions/Functions.ts | 2 ++ packages/jmespath/tests/unit/functions.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/jmespath/src/functions/Functions.ts b/packages/jmespath/src/functions/Functions.ts index 4385d8fbc9..c58d66a28d 100644 --- a/packages/jmespath/src/functions/Functions.ts +++ b/packages/jmespath/src/functions/Functions.ts @@ -171,6 +171,8 @@ class Functions { /** * Merge the provided objects into a single object. * + * Note that this is a shallow merge and will not merge nested objects. + * * @param args The objects to merge * @returns The merged object */ diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/functions.test.ts index 198c9addbb..c2544f6f25 100644 --- a/packages/jmespath/tests/unit/functions.test.ts +++ b/packages/jmespath/tests/unit/functions.test.ts @@ -724,7 +724,7 @@ describe('Functions tests', () => { expect(() => search(expression, data)).toThrow(error); }); - /* it.each([ + it.each([ { expression: 'merge(`{}`)', expected: {}, @@ -783,7 +783,7 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); } - ); */ + ); it.each([ { From 14a42a9b825827367eefc4a5c7d34b04846ba57f Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 6 Feb 2024 17:00:43 +0100 Subject: [PATCH 067/103] feat: keys, values, sort functions --- packages/jmespath/src/Parser.ts | 2 +- packages/jmespath/src/functions/Functions.ts | 31 ++++++------ .../jmespath/src/functions/typeChecking.ts | 10 ++-- packages/jmespath/src/visitor/utils.ts | 19 +++++++ .../jmespath/tests/unit/functions.test.ts | 50 ++++++++++++------- 5 files changed, 71 insertions(+), 41 deletions(-) diff --git a/packages/jmespath/src/Parser.ts b/packages/jmespath/src/Parser.ts index 1dde79c501..d5bfa85fbc 100644 --- a/packages/jmespath/src/Parser.ts +++ b/packages/jmespath/src/Parser.ts @@ -40,7 +40,7 @@ import type { Node, Token } from './types'; * If you don't want to read the full paper, there are some other good * overviews that explain the general idea: * - [Pratt Parsers: Expression Parsing Made Easy](https://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/) - * - [Simple Top-Down Parsing in Python](http://effbot.org/zone/simple-top-down-parsing.htm) + * - [Simple Top-Down Parsing in Python](https://11l-lang.org/archive/simple-top-down-parsing/) * - [Top Down Operator Precedence](http://javascript.crockford.com/tdop/tdop.html) */ class Parser { diff --git a/packages/jmespath/src/functions/Functions.ts b/packages/jmespath/src/functions/Functions.ts index c58d66a28d..a1e53e821d 100644 --- a/packages/jmespath/src/functions/Functions.ts +++ b/packages/jmespath/src/functions/Functions.ts @@ -1,4 +1,4 @@ -import { Expression, isNumber, isRecord } from '../visitor/utils'; +import { Expression, getType, isNumber, isRecord } from '../visitor/utils'; import type { JSONArray, JSONObject, JSONValue } from '../types'; import { typeCheck, arityCheck } from './typeChecking'; @@ -234,6 +234,19 @@ class Functions { : arg.split('').reverse().join(''); } + /** + * Sort the provided array. + * + * @param arg The array to sort + * @returns The sorted array + */ + @Functions.signature({ + argumentsSpecs: [['array-number', 'array-string']], + }) + public funcSort(arg: Array): Array { + return arg.sort(); + } + /** * Determines if the provided string starts with the provided suffix. * @@ -330,21 +343,7 @@ class Functions { argumentsSpecs: [['any']], }) public funcType(arg: JSONValue): string { - if (Array.isArray(arg)) { - return 'array'; - } else if (isRecord(arg)) { - return 'object'; - } else if (typeof arg === 'string') { - return 'string'; - } else if (typeof arg === 'number') { - return 'number'; - } else if (typeof arg === 'boolean') { - return 'boolean'; - } else if (Object.is(arg, null)) { - return 'null'; - } else { - return 'unknown'; - } + return getType(arg); } /** diff --git a/packages/jmespath/src/functions/typeChecking.ts b/packages/jmespath/src/functions/typeChecking.ts index c09c3b6fcb..7872a8a064 100644 --- a/packages/jmespath/src/functions/typeChecking.ts +++ b/packages/jmespath/src/functions/typeChecking.ts @@ -1,4 +1,4 @@ -import { isRecord } from '../visitor/utils'; +import { getType, isRecord } from '../visitor/utils'; import { JMESPathTypeError, ArityError, VariadicArityError } from '../errors'; /** @@ -78,7 +78,7 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { throw new JMESPathTypeError({ currentValue: arg, expectedTypes: argumentSpec, - actualType: Object.is(arg, null) ? 'null' : typeof arg, + actualType: getType(arg), }); } if (type.includes('-')) { @@ -103,7 +103,7 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { throw new JMESPathTypeError({ currentValue: arg, expectedTypes: argumentSpec, - actualType: type === 'boolean' ? 'boolean' : typeof arg, // TODO: fix this + actualType: getType(arg), }); } continue; @@ -115,7 +115,7 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { throw new JMESPathTypeError({ currentValue: arg, expectedTypes: argumentSpec, - actualType: typeof arg, + actualType: getType(arg), }); } continue; @@ -127,7 +127,7 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { throw new JMESPathTypeError({ currentValue: arg, expectedTypes: argumentSpec, - actualType: typeof arg, + actualType: getType(arg), }); } continue; diff --git a/packages/jmespath/src/visitor/utils.ts b/packages/jmespath/src/visitor/utils.ts index 3df8e15142..6c2c882037 100644 --- a/packages/jmespath/src/visitor/utils.ts +++ b/packages/jmespath/src/visitor/utils.ts @@ -205,6 +205,24 @@ const sliceArray = ( return result; }; +const getType = (value: unknown): string => { + if (Array.isArray(value)) { + return 'array'; + } else if (isRecord(value)) { + return 'object'; + } else if (typeof value === 'string') { + return 'string'; + } else if (isNumber(value)) { + return 'number'; + } else if (typeof value === 'boolean') { + return 'boolean'; + } else if (Object.is(value, null)) { + return 'null'; + } else { + return 'unknown'; + } +}; + export { Expression, isRecord, @@ -213,4 +231,5 @@ export { isNumber, isIntegerNumber, sliceArray, + getType, }; diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/functions.test.ts index c2544f6f25..8574eb9646 100644 --- a/packages/jmespath/tests/unit/functions.test.ts +++ b/packages/jmespath/tests/unit/functions.test.ts @@ -935,12 +935,11 @@ describe('Functions tests', () => { expect(result).toStrictEqual(expected); }); - /* it.each([ + it.each([ { expression: 'sort(keys(objects))', expected: ['bar', 'foo'], }, - { expression: 'sort(values(objects))', expected: ['bar', 'baz'], @@ -961,6 +960,10 @@ describe('Functions tests', () => { expression: 'sort(decimals)', expected: [-1.5, 1.01, 1.2], }, + { + expression: 'sort(empty_list)', + expected: [], + }, ])( 'should support the sort(), key(), and values() functions', ({ expression, expected }) => { @@ -989,61 +992,70 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); } - ); */ + ); - /* it.each([ + it.each([ { expression: 'keys(foo)', error: - 'TypeError: keys() expected argument 1 to be type (object) but received type number instead.', + 'Invalid argument type for function keys(), expected "object" but found "number" in expression: keys(foo)', }, { expression: 'keys(strings)', error: - 'TypeError: keys() expected argument 1 to be type (object) but received type array instead.', + 'Invalid argument type for function keys(), expected "object" but found "array" in expression: keys(strings)', }, { expression: 'keys(`false`)', error: - 'TypeError: keys() expected argument 1 to be type (object) but received type boolean instead.', + 'Invalid argument type for function keys(), expected "object" but found "boolean" in expression: keys(`false`)', }, { expression: 'values(foo)', error: - 'TypeError: values() expected argument 1 to be type (object) but received type number instead.', + 'Invalid argument type for function values(), expected "object" but found "number" in expression: values(foo)', }, { expression: 'sort(array)', error: - 'TypeError: sort() expected argument 1 to be type (Array | Array) but received type array instead.', + 'Invalid argument type for function sort(), expected "number" but found "string" in expression: sort(array)', }, { expression: 'sort(abc)', error: - 'TypeError: sort() expected argument 1 to be type (Array | Array) but received type null instead.', - }, - { - expression: 'sort(empty_list)', - expected: [], + 'Invalid argument type for function sort(), expected one of "array-number", "array-string" but found "null" in expression: sort(abc)', }, { expression: 'sort(@)', error: - 'TypeError: sort() expected argument 1 to be type (Array | Array) but received type object instead.', + 'Invalid argument type for function sort(), expected one of "array-number", "array-string" but found "object" in expression: sort(@)', }, ])( 'sort(), keys(), and values() function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in sort(), keys(), values() errors tests // Prepare const data = { - type: 'object', + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, }; // Act & Assess expect(() => search(expression, data)).toThrow(error); } - ); */ + ); /* it.each([ { @@ -1228,7 +1240,7 @@ describe('Functions tests', () => { { expression: 'starts_with(str, `0`)', error: - 'Invalid argument type for function starts_with(), expected "string" but found "object" in expression: starts_with(str, `0`)', + 'Invalid argument type for function starts_with(), expected "string" but found "null" in expression: starts_with(str, `0`)', }, ])('starts_with() function errors', ({ expression, error }) => { // Prepare From 5cc47757646b31c83a3bba92483c148065b926f3 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 6 Feb 2024 18:07:40 +0100 Subject: [PATCH 068/103] feat: join function --- packages/jmespath/src/functions/Functions.ts | 14 ++++++++ .../jmespath/tests/unit/functions.test.ts | 34 +++++++++++++------ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/packages/jmespath/src/functions/Functions.ts b/packages/jmespath/src/functions/Functions.ts index a1e53e821d..0ec58a1530 100644 --- a/packages/jmespath/src/functions/Functions.ts +++ b/packages/jmespath/src/functions/Functions.ts @@ -99,6 +99,20 @@ class Functions { return Math.floor(args); } + /** + * Join the provided array into a single string. + * + * @param separator The separator to use + * @param items The array of itmes to join + * @returns The joined array + */ + @Functions.signature({ + argumentsSpecs: [['string'], ['array-string']], + }) + public funcJoin(separator: string, items: Array): string { + return items.join(separator); + } + /** * Get the keys of the provided object. * diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/functions.test.ts index 8574eb9646..973e9a7c01 100644 --- a/packages/jmespath/tests/unit/functions.test.ts +++ b/packages/jmespath/tests/unit/functions.test.ts @@ -1057,7 +1057,7 @@ describe('Functions tests', () => { } ); - /* it.each([ + it.each([ { expression: `join(', ', strings)`, expected: 'a, b, c', @@ -1107,39 +1107,51 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); */ + }); - /* it.each([ + it.each([ { expression: 'join(\',\', `["a", 0]`)', error: - 'TypeError: join() expected argument 2 to be type (Array) but received type array instead.', + 'Invalid argument type for function join(), expected "string" but found "number" in expression: join(\',\', `["a", 0]`)', }, { expression: `join(', ', str)`, - error: - 'TypeError: join() expected argument 2 to be type (Array) but received type string instead.', + error: `Invalid argument type for function join(), expected "array-string" but found "string" in expression: join(', ', str)`, }, { expression: 'join(`2`, strings)', error: - 'TypeError: join() expected argument 1 to be type (string) but received type number instead.', + 'Invalid argument type for function join(), expected "string" but found "number" in expression: join(`2`, strings)', }, { expression: `join('|', decimals)`, error: - 'TypeError: join() expected argument 2 to be type (Array) but received type array instead.', + 'Invalid argument type for function join(), expected "string" but found "number" in expression: join(\'|\', decimals)', }, ])('join() function errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in join() errors tests // Prepare const data = { - type: 'object', + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, }; // Act & Assess expect(() => search(expression, data)).toThrow(error); - }); */ + }); /* it.each([ { From 4672bf55e6de570c5dd110510522231669136f72 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 7 Feb 2024 19:24:55 +0100 Subject: [PATCH 069/103] feat: reverse function --- packages/jmespath/tests/unit/functions.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/functions.test.ts index 973e9a7c01..5957a9bd06 100644 --- a/packages/jmespath/tests/unit/functions.test.ts +++ b/packages/jmespath/tests/unit/functions.test.ts @@ -1153,7 +1153,7 @@ describe('Functions tests', () => { expect(() => search(expression, data)).toThrow(error); }); - /* it.each([ + it.each([ { expression: 'reverse(numbers)', expected: [5, 4, 3, -1], @@ -1199,7 +1199,7 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); */ + }); it.each([ { From ecb5f8bcebc75c196e31bd7856458aa9c560eeb0 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 7 Feb 2024 20:05:22 +0100 Subject: [PATCH 070/103] feat: sort_by function --- packages/jmespath/src/functions/Functions.ts | 49 +++++++++++++++++++ .../jmespath/src/functions/typeChecking.ts | 16 +++++- .../jmespath/tests/unit/functions.test.ts | 37 ++++++++------ 3 files changed, 86 insertions(+), 16 deletions(-) diff --git a/packages/jmespath/src/functions/Functions.ts b/packages/jmespath/src/functions/Functions.ts index 0ec58a1530..afefeb68c4 100644 --- a/packages/jmespath/src/functions/Functions.ts +++ b/packages/jmespath/src/functions/Functions.ts @@ -1,6 +1,7 @@ import { Expression, getType, isNumber, isRecord } from '../visitor/utils'; import type { JSONArray, JSONObject, JSONValue } from '../types'; import { typeCheck, arityCheck } from './typeChecking'; +import { JMESPathTypeError } from '../errors'; /** * TODO: validate SignatureDecorator type and extract to a separate file @@ -261,6 +262,54 @@ class Functions { return arg.sort(); } + /** + * Sort the provided array by the provided expression. + * + * @param arg The array to sort + * @param expression The expression to sort by + * @returns The sorted array + */ + @Functions.signature({ + argumentsSpecs: [['array'], ['expression']], + }) + public funcSortBy( + args: Array, + expression: Expression + ): Array { + return args + .map((value, index) => { + const visited = expression.visit(value); + const type = getType(visited); + if (type !== 'string' && type !== 'number') { + throw new JMESPathTypeError({ + currentValue: visited, + expectedTypes: ['string'], + actualType: getType(visited), + }); + } + + return { + value, + index, + visited: visited ? visited : null, + }; + }) + .sort((a, b) => { + if (a.visited === null && b.visited === null) { + return 0; + } else if (a.visited === null) { + return -1; + } else if (b.visited === null) { + return 1; + } else if (a.visited === b.visited) { + return a.index - b.index; // Make the sort stable + } else { + return a.visited > b.visited ? 1 : -1; + } + }) + .map(({ value }) => value); // Extract the original values + } + /** * Determines if the provided string starts with the provided suffix. * diff --git a/packages/jmespath/src/functions/typeChecking.ts b/packages/jmespath/src/functions/typeChecking.ts index 7872a8a064..fac4f88ddf 100644 --- a/packages/jmespath/src/functions/typeChecking.ts +++ b/packages/jmespath/src/functions/typeChecking.ts @@ -1,4 +1,4 @@ -import { getType, isRecord } from '../visitor/utils'; +import { Expression, getType, isRecord } from '../visitor/utils'; import { JMESPathTypeError, ArityError, VariadicArityError } from '../errors'; /** @@ -97,7 +97,19 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { } break; } else { - if (type === 'string' || type === 'number' || type === 'boolean') { + if (type === 'expression') { + if (!(arg instanceof Expression)) { + if (!hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); + } + continue; + } + break; + } else if (type === 'string' || type === 'number' || type === 'boolean') { if (typeof arg !== type) { if (!hasMoreTypesToCheck) { throw new JMESPathTypeError({ diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/functions.test.ts index 5957a9bd06..d9e23311f2 100644 --- a/packages/jmespath/tests/unit/functions.test.ts +++ b/packages/jmespath/tests/unit/functions.test.ts @@ -1606,7 +1606,7 @@ describe('Functions tests', () => { } ); - /* it.each([ + it.each([ { description: 'sort by field expression', expression: 'sort_by(people, &age)', @@ -1734,6 +1734,16 @@ describe('Functions tests', () => { expression: 'sort_by(`[]`, &age)', expected: [], }, + { + expression: 'sort_by(people, &name)', + expected: [ + { age: 10, age_str: '10', bool: true, name: 3 }, + { age: 20, age_str: '20', bool: true, name: 'a', extra: 'foo' }, + { age: 40, age_str: '40', bool: false, name: 'b', extra: 'bar' }, + { age: 30, age_str: '30', bool: true, name: 'c' }, + { age: 50, age_str: '50', bool: false, name: 'd' }, + ], + }, ])('should support sorty_by() special cases', ({ expression, expected }) => { // Prepare const data = { @@ -1778,27 +1788,25 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); */ - /* it.each([ + }); + + it.each([ { expression: 'sort_by(people, &extra)', - error: 'TypeError: expected (string), received null', + error: + 'Invalid argument type for function sort_by(), expected "string" but found "null" in expression: sort_by(people, &extra)', }, { expression: 'sort_by(people, &bool)', - error: 'TypeError: unexpected type (boolean)', - }, - { - expression: 'sort_by(people, &name)', - error: 'TypeError: expected (string), received number', + error: + 'Invalid argument type for function sort_by(), expected "string" but found "boolean" in expression: sort_by(people, &bool)', }, { expression: 'sort_by(people, name)', error: - 'TypeError: sort_by() expected argument 2 to be type (expression) but received type null instead.', + 'Invalid argument type for function sort_by(), expected "expression" but found "null" in expression: sort_by(people, name)', }, ])('sort_by() function special cases errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in sort_by() function special cases errors tests // Prepare const data = { people: [ @@ -1839,7 +1847,8 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); - }); */ + }); + /* it.each([ { expression: 'max_by(people, &age)', @@ -2091,7 +2100,7 @@ describe('Functions tests', () => { expect(() => search(expression, data)).toThrow(error); }); */ - /* it.each([ + it.each([ { description: 'stable sort order', expression: 'sort_by(people, &age)', @@ -2198,7 +2207,7 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); */ + }); it.each([ { From dad71c31103933e41553084ead9b518d9b334a1a Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sat, 10 Feb 2024 16:49:17 +0100 Subject: [PATCH 071/103] feat: max_by and min_by function --- packages/jmespath/src/functions/Functions.ts | 104 ++++++++++++++++++ .../jmespath/tests/unit/functions.test.ts | 33 +++--- 2 files changed, 123 insertions(+), 14 deletions(-) diff --git a/packages/jmespath/src/functions/Functions.ts b/packages/jmespath/src/functions/Functions.ts index afefeb68c4..7a64725657 100644 --- a/packages/jmespath/src/functions/Functions.ts +++ b/packages/jmespath/src/functions/Functions.ts @@ -183,6 +183,58 @@ class Functions { } } + /** + * Get the item in the provided array that has the maximum value when the provided expression is evaluated. + * + * @param args The array of items to get the maximum value of + * @param expression The expression to evaluate for each item in the array + * @returns The item in the array that has the maximum value when the expression is evaluated + */ + @Functions.signature({ + argumentsSpecs: [['array'], ['expression']], + }) + public funcMaxBy( + args: Array, + expression: Expression + ): JSONObject | null { + if (args.length === 0) { + return null; + } + + const visitedArgs = args.map((arg) => ({ + arg, + visited: expression.visit(arg), + })); + + const max = visitedArgs.reduce((max, current) => { + const type = getType(current.visited); + if (type !== 'string' && type !== 'number') { + throw new JMESPathTypeError({ + currentValue: current.visited, + expectedTypes: ['string'], + actualType: type, + }); + } + + if ( + (max.visited === null || max.visited === undefined) && + (current.visited === null || current.visited === undefined) + ) { + return max; + } else if (max.visited === null || max.visited === undefined) { + return current; + } else if (current.visited === null || current.visited === undefined) { + return max; + } else if (max.visited === current.visited) { + return max; + } else { + return max.visited > current.visited ? max : current; + } + }, visitedArgs[0]); + + return max.arg; + } + /** * Merge the provided objects into a single object. * @@ -219,6 +271,58 @@ class Functions { } } + /** + * Get the item in the provided array that has the minimum value when the provided expression is evaluated. + * + * @param args The array of items to get the minimum value of + * @param expression The expression to evaluate for each item in the array + * @returns The item in the array that has the minimum value when the expression is evaluated + */ + @Functions.signature({ + argumentsSpecs: [['array'], ['expression']], + }) + public funcMinBy( + args: Array, + expression: Expression + ): JSONObject | null { + if (args.length === 0) { + return null; + } + + const visitedArgs = args.map((arg) => ({ + arg, + visited: expression.visit(arg), + })); + + const min = visitedArgs.reduce((min, current) => { + const type = getType(current.visited); + if (type !== 'string' && type !== 'number') { + throw new JMESPathTypeError({ + currentValue: current.visited, + expectedTypes: ['string'], + actualType: type, + }); + } + + if ( + (min.visited === null || min.visited === undefined) && + (current.visited === null || current.visited === undefined) + ) { + return min; + } else if (min.visited === null || min.visited === undefined) { + return current; + } else if (current.visited === null || current.visited === undefined) { + return min; + } else if (min.visited === current.visited) { + return min; + } else { + return min.visited < current.visited ? min : current; + } + }, visitedArgs[0]); + + return min.arg; + } + /** * Get the first argument that does not evaluate to null. * If all arguments evaluate to null, then null is returned. diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/functions.test.ts index d9e23311f2..013d24f60a 100644 --- a/packages/jmespath/tests/unit/functions.test.ts +++ b/packages/jmespath/tests/unit/functions.test.ts @@ -1849,7 +1849,7 @@ describe('Functions tests', () => { expect(() => search(expression, data)).toThrow(error); }); - /* it.each([ + it.each([ { expression: 'max_by(people, &age)', expected: { @@ -1921,18 +1921,20 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); */ - /* it.each([ + }); + + it.each([ { expression: 'max_by(people, &bool)', - error: 'TypeError: expected one of (number | string), received boolean', + error: + 'Invalid argument type for function max_by(), expected "string" but found "boolean" in expression: max_by(people, &bool)', }, { expression: 'max_by(people, &extra)', - error: 'TypeError: expected one of (number | string), received null', + error: + 'Invalid argument type for function max_by(), expected "string" but found "null" in expression: max_by(people, &extra)', }, ])('max_by() function special cases errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in max_by() function special cases errors tests // Prepare const data = { people: [ @@ -1973,8 +1975,9 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); - }); */ - /* it.each([ + }); + + it.each([ { expression: 'min_by(people, &age)', expected: { @@ -2046,18 +2049,20 @@ describe('Functions tests', () => { // Assess expect(result).toStrictEqual(expected); - }); */ - /* it.each([ + }); + + it.each([ { expression: 'min_by(people, &bool)', - error: 'TypeError: expected one of (number | string), received boolean', + error: + 'Invalid argument type for function min_by(), expected "string" but found "boolean" in expression: min_by(people, &bool)', }, { expression: 'min_by(people, &extra)', - error: 'TypeError: expected one of (number | string), received null', + error: + 'Invalid argument type for function min_by(), expected "string" but found "null" in expression: min_by(people, &extra)', }, ])('min_by() function special cases errors', ({ expression, error }) => { - // TODO: see if we can assert the error type as well in min_by() function special cases errors tests // Prepare const data = { people: [ @@ -2099,7 +2104,7 @@ describe('Functions tests', () => { // Act & Assess expect(() => search(expression, data)).toThrow(error); }); - */ + it.each([ { description: 'stable sort order', From 9f4d4fdc6ebeb8cfd90c53eba0c0b92912e5da38 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 23 Feb 2024 10:22:12 +0100 Subject: [PATCH 072/103] chore: test setup --- .../{jest.config.js => jest.config.cjs} | 0 packages/jmespath/package.json | 2 +- packages/jmespath/src/compile.ts | 23 ------------------- 3 files changed, 1 insertion(+), 24 deletions(-) rename packages/jmespath/{jest.config.js => jest.config.cjs} (100%) delete mode 100644 packages/jmespath/src/compile.ts diff --git a/packages/jmespath/jest.config.js b/packages/jmespath/jest.config.cjs similarity index 100% rename from packages/jmespath/jest.config.js rename to packages/jmespath/jest.config.cjs diff --git a/packages/jmespath/package.json b/packages/jmespath/package.json index 85b4788f75..e16c809258 100644 --- a/packages/jmespath/package.json +++ b/packages/jmespath/package.json @@ -9,7 +9,7 @@ "private": true, "scripts": { "test": "npm run test:unit", - "test:unit": "jest --group=unit/jmespath --detectOpenHandles", + "test:unit": "jest --group=unit --detectOpenHandles --coverage --verbose", "test:e2e": "echo 'Not applicable for this package'", "watch": "jest --watch", "build": "tsc", diff --git a/packages/jmespath/src/compile.ts b/packages/jmespath/src/compile.ts deleted file mode 100644 index afa8d122d6..0000000000 --- a/packages/jmespath/src/compile.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Parser } from './Parser'; -import type { ParsedResult } from './ParsedResult'; - -/** - * TODO: see if this Expression type should be the return of compile() - */ -/* type Expression = { - type: string; - children: Expression[]; - value: string; -}; */ - -/** - * TODO: write docs for compile() - * TODO: fix types for compile() - * - * @param expression The JMESPath expression to compile. - */ -const compile = (expression: string): ParsedResult => { - return new Parser().parse(expression); -}; - -export { compile }; From 76b5c671aebefb22630ad30772cb42f660bb00f3 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Sat, 24 Feb 2024 01:12:46 +0100 Subject: [PATCH 073/103] tests: coverage tests --- package-lock.json | 17 +- packages/commons/package.json | 8 + packages/commons/src/guards.ts | 52 ---- packages/commons/src/index.ts | 12 +- packages/commons/src/typeUtils.ts | 160 ++++++++++ packages/jmespath/README.md | 74 +++++ packages/jmespath/jest.config.cjs | 10 +- packages/jmespath/package.json | 56 +++- packages/jmespath/src/Expression.ts | 29 ++ .../jmespath/src/{functions => }/Functions.ts | 155 ++++++---- packages/jmespath/src/Lexer.ts | 96 +++--- packages/jmespath/src/ParsedResult.ts | 15 +- packages/jmespath/src/Parser.ts | 168 ++++++----- .../src/{visitor => }/TreeInterpreter.ts | 283 ++++++++++-------- packages/jmespath/src/ast.ts | 185 +++++++----- packages/jmespath/src/constants.ts | 39 ++- packages/jmespath/src/errors.ts | 44 +-- packages/jmespath/src/functions/index.ts | 1 - .../jmespath/src/functions/typeChecking.ts | 153 ---------- packages/jmespath/src/index.ts | 13 +- packages/jmespath/src/search.ts | 57 +++- packages/jmespath/src/types.ts | 101 +++++++ packages/jmespath/src/types/AST.ts | 12 - packages/jmespath/src/types/JSON.ts | 6 - packages/jmespath/src/types/ParsedResult.ts | 8 - packages/jmespath/src/types/Token.ts | 14 - .../jmespath/src/types/TreeInterpreter.ts | 8 - packages/jmespath/src/types/index.ts | 5 - packages/jmespath/src/utils.ts | 257 ++++++++++++++++ packages/jmespath/src/visitor/index.ts | 1 - packages/jmespath/src/visitor/utils.ts | 235 --------------- .../tests/unit/{ => compliance}/base.test.ts | 2 +- .../unit/{ => compliance}/boolean.test.ts | 2 +- .../unit/{ => compliance}/current.test.ts | 2 +- .../unit/{ => compliance}/escape.test.ts | 2 +- .../unit/{ => compliance}/filters.test.ts | 2 +- .../unit/{ => compliance}/functions.test.ts | 2 +- .../unit/{ => compliance}/identifiers.test.ts | 2 +- .../unit/{ => compliance}/indices.test.ts | 2 +- .../unit/{ => compliance}/literal.test.ts | 2 +- .../unit/{ => compliance}/multiselect.test.ts | 2 +- .../tests/unit/{ => compliance}/pipe.test.ts | 2 +- .../tests/unit/{ => compliance}/slice.test.ts | 4 +- .../unit/{ => compliance}/syntax.test.ts | 2 +- .../unit/{ => compliance}/unicode.test.ts | 2 +- .../unit/{ => compliance}/wildcard.test.ts | 2 +- packages/jmespath/tests/unit/index.test.ts | 260 +++++++++++++++- packages/jmespath/tsconfig.esm.json | 12 + packages/jmespath/tsconfig.json | 38 +-- packages/jmespath/typedoc.json | 7 + 50 files changed, 1613 insertions(+), 1010 deletions(-) delete mode 100644 packages/commons/src/guards.ts create mode 100644 packages/commons/src/typeUtils.ts create mode 100644 packages/jmespath/README.md create mode 100644 packages/jmespath/src/Expression.ts rename packages/jmespath/src/{functions => }/Functions.ts (80%) rename packages/jmespath/src/{visitor => }/TreeInterpreter.ts (65%) delete mode 100644 packages/jmespath/src/functions/index.ts delete mode 100644 packages/jmespath/src/functions/typeChecking.ts create mode 100644 packages/jmespath/src/types.ts delete mode 100644 packages/jmespath/src/types/AST.ts delete mode 100644 packages/jmespath/src/types/JSON.ts delete mode 100644 packages/jmespath/src/types/ParsedResult.ts delete mode 100644 packages/jmespath/src/types/Token.ts delete mode 100644 packages/jmespath/src/types/TreeInterpreter.ts delete mode 100644 packages/jmespath/src/types/index.ts create mode 100644 packages/jmespath/src/utils.ts delete mode 100644 packages/jmespath/src/visitor/index.ts delete mode 100644 packages/jmespath/src/visitor/utils.ts rename packages/jmespath/tests/unit/{ => compliance}/base.test.ts (98%) rename packages/jmespath/tests/unit/{ => compliance}/boolean.test.ts (99%) rename packages/jmespath/tests/unit/{ => compliance}/current.test.ts (95%) rename packages/jmespath/tests/unit/{ => compliance}/escape.test.ts (97%) rename packages/jmespath/tests/unit/{ => compliance}/filters.test.ts (99%) rename packages/jmespath/tests/unit/{ => compliance}/functions.test.ts (99%) rename packages/jmespath/tests/unit/{ => compliance}/identifiers.test.ts (99%) rename packages/jmespath/tests/unit/{ => compliance}/indices.test.ts (99%) rename packages/jmespath/tests/unit/{ => compliance}/literal.test.ts (99%) rename packages/jmespath/tests/unit/{ => compliance}/multiselect.test.ts (99%) rename packages/jmespath/tests/unit/{ => compliance}/pipe.test.ts (98%) rename packages/jmespath/tests/unit/{ => compliance}/slice.test.ts (96%) rename packages/jmespath/tests/unit/{ => compliance}/syntax.test.ts (99%) rename packages/jmespath/tests/unit/{ => compliance}/unicode.test.ts (97%) rename packages/jmespath/tests/unit/{ => compliance}/wildcard.test.ts (99%) create mode 100644 packages/jmespath/tsconfig.esm.json create mode 100644 packages/jmespath/typedoc.json diff --git a/package-lock.json b/package-lock.json index 48e25c231d..d98212b4bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,8 @@ "docs/snippets", "layers", "examples/cdk", - "examples/sam" + "examples/sam", + "packages/jmespath" ], "devDependencies": { "@middy/core": "^4.7.0", @@ -292,6 +293,10 @@ "resolved": "packages/idempotency", "link": true }, + "node_modules/@aws-lambda-powertools/jmespath": { + "resolved": "packages/jmespath", + "link": true + }, "node_modules/@aws-lambda-powertools/logger": { "resolved": "packages/logger", "link": true @@ -17202,6 +17207,14 @@ } } }, + "packages/jmespath": { + "name": "@aws-lambda-powertools/jmespath", + "version": "1.18.1", + "license": "MIT-0", + "dependencies": { + "@aws-lambda-powertools/commons": "^1.18.1" + } + }, "packages/logger": { "name": "@aws-lambda-powertools/logger", "version": "2.0.2", @@ -17331,4 +17344,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/commons/package.json b/packages/commons/package.json index 77a2908835..3256503909 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -40,6 +40,10 @@ "default": "./lib/esm/index.js" } }, + "./typeutils": { + "import": "./lib/esm/typeUtils.js", + "require": "./lib/cjs/typeUtils.js" + }, "./types": { "import": "./lib/esm/types/index.js", "require": "./lib/cjs/types/index.js" @@ -50,6 +54,10 @@ "types": [ "lib/cjs/types/index.d.ts", "lib/esm/types/index.d.ts" + ], + "typeutils": [ + "lib/cjs/typeUtils.d.ts", + "lib/esm/typeUtils.d.ts" ] } }, diff --git a/packages/commons/src/guards.ts b/packages/commons/src/guards.ts deleted file mode 100644 index 7776280e8b..0000000000 --- a/packages/commons/src/guards.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Returns true if the passed value is a record (object). - * - * @param value - */ -const isRecord = (value: unknown): value is Record => { - return ( - Object.prototype.toString.call(value) === '[object Object]' && - !Object.is(value, null) - ); -}; - -/** - * Returns true if the passed value is truthy. - * - * @param value - */ -const isTruthy = (value: unknown): boolean => { - if (typeof value === 'string') { - return value !== ''; - } else if (typeof value === 'number') { - return value !== 0; - } else if (typeof value === 'boolean') { - return value; - } else if (Array.isArray(value)) { - return value.length > 0; - } else if (isRecord(value)) { - return Object.keys(value).length > 0; - } else { - return false; - } -}; - -/** - * Returns true if the passed value is null or undefined. - * - * @param value - */ -const isNullOrUndefined = (value: unknown): value is null | undefined => { - return Object.is(value, null) || Object.is(value, undefined); -}; - -/** - * Returns true if the passed value is a string. - * @param value - * @returns - */ -const isString = (value: unknown): value is string => { - return typeof value === 'string'; -}; - -export { isRecord, isString, isTruthy, isNullOrUndefined }; diff --git a/packages/commons/src/index.ts b/packages/commons/src/index.ts index cdf0815bad..591ad8ccff 100644 --- a/packages/commons/src/index.ts +++ b/packages/commons/src/index.ts @@ -1,4 +1,14 @@ -export { isRecord, isString, isTruthy, isNullOrUndefined } from './guards.js'; +export { + isRecord, + isString, + isNumber, + isIntegerNumber, + isTruthy, + isNull, + isNullOrUndefined, + getType, + isStrictEqual, +} from './typeUtils.js'; export { Utility } from './Utility.js'; export { EnvironmentVariablesService } from './config/EnvironmentVariablesService.js'; export { addUserAgentMiddleware, isSdkClient } from './awsSdkUtils.js'; diff --git a/packages/commons/src/typeUtils.ts b/packages/commons/src/typeUtils.ts new file mode 100644 index 0000000000..7e53de0b1c --- /dev/null +++ b/packages/commons/src/typeUtils.ts @@ -0,0 +1,160 @@ +/** + * Returns true if the passed value is a record (object). + * + * @param value The value to check + */ +const isRecord = (value: unknown): value is Record => { + return ( + Object.prototype.toString.call(value) === '[object Object]' && + !Object.is(value, null) + ); +}; + +/** + * Check if a value is a string. + * + * @param value The value to check + */ +const isString = (value: unknown): value is string => { + return typeof value === 'string'; +}; + +/** + * Check if a value is a number. + * + * @param value The value to check + */ +const isNumber = (value: unknown): value is number => { + return typeof value === 'number'; +}; + +/** + * Check if a value is an integer number. + * + * @param value The value to check + */ +const isIntegerNumber = (value: unknown): value is number => { + return isNumber(value) && Number.isInteger(value); +}; + +/** + * Check if a value is truthy. + * + * @param value The value to check + */ +const isTruthy = (value: unknown): boolean => { + if (isString(value)) { + return value !== ''; + } else if (isNumber(value)) { + return value !== 0; + } else if (typeof value === 'boolean') { + return value; + } else if (Array.isArray(value)) { + return value.length > 0; + } else if (isRecord(value)) { + return Object.keys(value).length > 0; + } else { + return false; + } +}; + +/** + * Check if a value is null. + * + * @param value The value to check + */ +const isNull = (value: unknown): value is null => { + return Object.is(value, null); +}; + +/** + * Check if a value is null or undefined. + * + * @param value The value to check + */ +const isNullOrUndefined = (value: unknown): value is null | undefined => { + return isNull(value) || Object.is(value, undefined); +}; + +/** + * Get the type of a value as a string. + * + * @param value The value to check + */ +const getType = (value: unknown): string => { + if (Array.isArray(value)) { + return 'array'; + } else if (isRecord(value)) { + return 'object'; + } else if (isString(value)) { + return 'string'; + } else if (isNumber(value)) { + return 'number'; + } else if (typeof value === 'boolean') { + return 'boolean'; + } else if (isNull(value)) { + return 'null'; + } else { + return 'unknown'; + } +}; + +/** + * Check if two unknown values are strictly equal. + * + * If the values are arrays, then each element is compared, regardless of + * order. If the values are objects, then each key and value from left + * is compared to the corresponding key and value from right. If the + * values are primitives, then they are compared using strict equality. + * + * @param left Left side of strict equality comparison + * @param right Right side of strict equality comparison + */ +const isStrictEqual = (left: unknown, right: unknown): boolean => { + if (left === right) { + return true; + } else if (typeof left !== typeof right) { + return false; + } else if (Array.isArray(left) && Array.isArray(right)) { + if (left.length !== right.length) { + return false; + } + for (const [i, value] of left.entries()) { + if (!isStrictEqual(value, right[i])) { + return false; + } + } + + return true; + } else if (isRecord(left) && isRecord(right)) { + const leftKeys = Object.keys(left); + const leftValues = Object.values(left); + const rightKeys = Object.keys(right); + const rightValues = Object.values(right); + if ( + leftKeys.length !== rightKeys.length || + leftValues.length !== rightValues.length + ) { + return false; + } + + return ( + isStrictEqual(leftKeys, rightKeys) && + isStrictEqual(leftValues, rightValues) + ); + } else { + return false; + } +}; + +export { + isRecord, + isString, + isNumber, + isIntegerNumber, + isTruthy, + isNull, + isNullOrUndefined, + getType, + isStrictEqual, +}; diff --git a/packages/jmespath/README.md b/packages/jmespath/README.md new file mode 100644 index 0000000000..4d5ab664b3 --- /dev/null +++ b/packages/jmespath/README.md @@ -0,0 +1,74 @@ +# Powertools for AWS Lambda (TypeScript) - JMESPath Utility + +Powertools for AWS Lambda (TypeScript) is a developer toolkit to implement Serverless [best practices and increase developer velocity](https://docs.powertools.aws.dev/lambda/typescript/latest/#features). + +You can use the package in both TypeScript and JavaScript code bases. + +- [Intro](#intro) +- [Usage](#usage) +- [Contribute](#contribute) +- [Roadmap](#roadmap) +- [Connect](#connect) +- [How to support Powertools for AWS Lambda (TypeScript)?](#how-to-support-powertools-for-aws-lambda-typescript) + - [Becoming a reference customer](#becoming-a-reference-customer) + - [Sharing your work](#sharing-your-work) + - [Using Lambda Layer](#using-lambda-layer) +- [License](#license) + +## Intro + +The JMESPath utility is a high-level function to parse and extract data from JSON objects using JMESPath expressions. + +## Usage + +To get started, install the library by running: + +```sh +npm i @aws-lambda-powertools/jmespath +``` + +## Contribute + +If you are interested in contributing to this project, please refer to our [Contributing Guidelines](https://github.com/aws-powertools/powertools-lambda-typescript/blob/main/CONTRIBUTING.md). + +## Roadmap + +The roadmap of Powertools for AWS Lambda (TypeScript) is driven by customers’ demand. +Help us prioritize upcoming functionalities or utilities by [upvoting existing RFCs and feature requests](https://github.com/aws-powertools/powertools-lambda-typescript/issues), or [creating new ones](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new/choose), in this GitHub repository. + +## Connect + +* **Powertools for AWS Lambda on Discord**: `#typescript` - **[Invite link](https://discord.gg/B8zZKbbyET)** +* **Email**: aws-lambda-powertools-feedback@amazon.com + +## How to support Powertools for AWS Lambda (TypeScript)? + +### Becoming a reference customer + +Knowing which companies are using this library is important to help prioritize the project internally. If your company is using Powertools for AWS Lambda (TypeScript), you can request to have your name and logo added to the README file by raising a [Support Powertools for AWS Lambda (TypeScript) (become a reference)](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new?assignees=&labels=customer-reference&template=support_powertools.yml&title=%5BSupport+Lambda+Powertools%5D%3A+%3Cyour+organization+name%3E) issue. + +The following companies, among others, use Powertools: + +* [Hashnode](https://hashnode.com/) +* [Trek10](https://www.trek10.com/) +* [Elva](https://elva-group.com) +* [globaldatanet](https://globaldatanet.com/) +* [Bailey Nelson](https://www.baileynelson.com.au) +* [Perfect Post](https://www.perfectpost.fr) +* [Sennder](https://sennder.com/) +* [Certible](https://www.certible.com/) +* [tecRacer GmbH & Co. KG](https://www.tecracer.com/) +* [AppYourself](https://appyourself.net) +* [Alma Media](https://www.almamedia.fi) + +### Sharing your work + +Share what you did with Powertools for AWS Lambda (TypeScript) 💞💞. Blog post, workshops, presentation, sample apps and others. Check out what the community has already shared about Powertools for AWS Lambda (TypeScript) [here](https://docs.powertools.aws.dev/lambda/typescript/latest/we_made_this). + +### Using Lambda Layer + +This helps us understand who uses Powertools for AWS Lambda (TypeScript) in a non-intrusive way, and helps us gain future investments for other Powertools for AWS Lambda languages. When [using Layers](https://docs.powertools.aws.dev/lambda/typescript/latest/#lambda-layer), you can add Powertools as a dev dependency to not impact the development process. + +## License + +This library is licensed under the MIT-0 License. See the LICENSE file. \ No newline at end of file diff --git a/packages/jmespath/jest.config.cjs b/packages/jmespath/jest.config.cjs index 5dbf28b722..9b345debe4 100644 --- a/packages/jmespath/jest.config.cjs +++ b/packages/jmespath/jest.config.cjs @@ -4,7 +4,9 @@ module.exports = { color: 'purple', }, runner: 'groups', - preset: 'ts-jest', + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, transform: { '^.+\\.ts?$': 'ts-jest', }, @@ -14,7 +16,11 @@ module.exports = { roots: ['/src', '/tests'], testPathIgnorePatterns: ['/node_modules/'], testEnvironment: 'node', - coveragePathIgnorePatterns: ['/node_modules/', '/types/'], + coveragePathIgnorePatterns: [ + '/node_modules/', + 'src/index.ts', + 'src/types/index.ts', + ], coverageThreshold: { global: { statements: 100, diff --git a/packages/jmespath/package.json b/packages/jmespath/package.json index e16c809258..a43a68e817 100644 --- a/packages/jmespath/package.json +++ b/packages/jmespath/package.json @@ -1,39 +1,65 @@ { "name": "@aws-lambda-powertools/jmespath", - "version": "1.12.1", - "description": "The JMESPath package for the Powertools for AWS Lambda (TypeScript) library", + "version": "1.18.1", + "description": "A type safe and modern jmespath module to parse and extract data from JSON documents using JMESPath", "author": { "name": "Amazon Web Services", "url": "https://aws.amazon.com" }, - "private": true, + "publishConfig": { + "access": "public" + }, "scripts": { "test": "npm run test:unit", - "test:unit": "jest --group=unit --detectOpenHandles --coverage --verbose", + "test:unit": "jest --group=unit --detectOpenHandles --coverage", + "jest": "jest --detectOpenHandles --verbose", "test:e2e": "echo 'Not applicable for this package'", - "watch": "jest --watch", - "build": "tsc", + "watch": "jest --watch --group=unit", + "build:cjs": "tsc --build tsconfig.json && echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", + "build:esm": "tsc --build tsconfig.esm.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json", + "build": "npm run build:esm & npm run build:cjs", "lint": "eslint --ext .ts,.js --no-error-on-unmatched-pattern .", "lint-fix": "eslint --fix --ext .ts,.js --no-error-on-unmatched-pattern .", - "prebuild": "rimraf ./lib", "prepack": "node ../../.github/scripts/release_patch_package_json.js ." }, "lint-staged": { - "*.{ts,js}": "npm run lint-fix" + "*.{js,ts}": "npm run lint-fix" }, + "homepage": "https://github.com/aws-powertools/powertools-lambda-typescript", + "license": "MIT-0", + "type": "module", "exports": { ".": { - "import": "./lib/index.js", - "require": "./lib/index.js" + "require": { + "types": "./lib/cjs/index.d.ts", + "default": "./lib/cjs/index.js" + }, + "import": { + "types": "./lib/esm/index.d.ts", + "default": "./lib/esm/index.js" + } + }, + "./types": { + "import": "./lib/esm/types.js", + "require": "./lib/cjs/types.js" } }, - "main": "./lib/index.js", - "types": "./lib/index.d.ts", - "homepage": "https://github.com/aws-powertools/powertools-lambda-typescript/tree/main/packages/jmespath#readme", - "license": "MIT-0", + "typesVersions": { + "*": { + "types": [ + "lib/cjs/types.d.ts", + "lib/esm/types.d.ts" + ] + } + }, + "types": "./lib/cjs/index.d.ts", + "main": "./lib/cjs/index.js", "files": [ "lib" ], + "dependencies": { + "@aws-lambda-powertools/commons": "^1.18.1" + }, "repository": { "type": "git", "url": "git+https://github.com/aws-powertools/powertools-lambda-typescript.git" @@ -46,8 +72,8 @@ "lambda", "powertools", "jmespath", - "functions", "serverless", + "typescript", "nodejs" ] } \ No newline at end of file diff --git a/packages/jmespath/src/Expression.ts b/packages/jmespath/src/Expression.ts new file mode 100644 index 0000000000..3efb27f680 --- /dev/null +++ b/packages/jmespath/src/Expression.ts @@ -0,0 +1,29 @@ +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; +import type { TreeInterpreter } from './TreeInterpreter.js'; +import type { Node } from './types.js'; + +/** + * Apply a JMESPath expression to a JSON value. + */ +class Expression { + readonly #expression: Node; + readonly #interpreter: TreeInterpreter; + + public constructor(expression: Node, interpreter: TreeInterpreter) { + this.#expression = expression; + this.#interpreter = interpreter; + } + + /** + * Evaluate the expression against a JSON value. + * + * @param value The JSON value to apply the expression to. + * @param node The node to visit. + * @returns The result of applying the expression to the value. + */ + public visit(value: JSONValue, node?: Node): JSONValue { + return this.#interpreter.visit(node ?? this.#expression, value); + } +} + +export { Expression }; diff --git a/packages/jmespath/src/functions/Functions.ts b/packages/jmespath/src/Functions.ts similarity index 80% rename from packages/jmespath/src/functions/Functions.ts rename to packages/jmespath/src/Functions.ts index 7a64725657..27c66bbb23 100644 --- a/packages/jmespath/src/functions/Functions.ts +++ b/packages/jmespath/src/Functions.ts @@ -1,32 +1,52 @@ -import { Expression, getType, isNumber, isRecord } from '../visitor/utils'; -import type { JSONArray, JSONObject, JSONValue } from '../types'; -import { typeCheck, arityCheck } from './typeChecking'; -import { JMESPathTypeError } from '../errors'; +import type { + JSONArray, + JSONObject, + JSONValue, +} from '@aws-lambda-powertools/commons/types'; +import { + getType, + isNumber, + isRecord, +} from '@aws-lambda-powertools/commons/typeutils'; +import type { Expression } from './Expression.js'; +import { JMESPathTypeError } from './errors.js'; +import type { + FunctionSignatureDecorator, + FunctionSignatureOptions, +} from './types.js'; +import { arityCheck, typeCheck } from './utils.js'; /** - * TODO: validate SignatureDecorator type and extract to a separate file - */ -type SignatureDecorator = ( - target: Functions, - propertyKey: string | symbol, - descriptor: PropertyDescriptor -) => void; - -/** - * TODO: validate SignatureOptions type and extract to a separate file - */ -type SignatureOptions = { - argumentsSpecs: Array>; - variadic?: boolean; -}; - -/** - * TODO: write docs for Functions + * A class that contains the built-in JMESPath functions. + * + * The built-in functions are implemented as methods on the Functions class. + * Each method is decorated with the `@Function.signature()` decorator to enforce the + * arity and types of the arguments passed to the function at runtime. + * + * You can extend the Functions class to add custom functions by creating a new class + * that extends Functions and adding new methods to it. + * + * @example + * ```typescript + * import { search } from '@aws-lambda-powertools/jmespath'; + * import { Functions } from '@aws-lambda-powertools/jmespath/functions'; + * + * class MyFunctions extends Functions { + * ⁣@Functions.signature({ + * argumentsSpecs: [['number'], ['number']], + * variadic: true, + * }) + * public funcMyMethod(args: Array): unknown { + * // ... + * } + * } + * + * const myFunctions = new MyFunctions(); + * + * search('myMethod(@)', {}, { customFunctions: new MyFunctions() }); + * ``` */ class Functions { - // TODO: find a type for FUNCTION_TABLE - public FUNCTION_TABLE: Map = new Map(); - /** * Get the absolute value of the provided number. * @@ -216,19 +236,15 @@ class Functions { }); } - if ( - (max.visited === null || max.visited === undefined) && - (current.visited === null || current.visited === undefined) - ) { - return max; - } else if (max.visited === null || max.visited === undefined) { - return current; - } else if (current.visited === null || current.visited === undefined) { - return max; - } else if (max.visited === current.visited) { + if (max.visited === current.visited) { return max; } else { - return max.visited > current.visited ? max : current; + // We can safely cast visited to number | string here because we've already + // checked the type at runtime above and we know that it's either a number or a string + return (max.visited as number | string) > + (current.visited as number | string) + ? max + : current; } }, visitedArgs[0]); @@ -304,19 +320,15 @@ class Functions { }); } - if ( - (min.visited === null || min.visited === undefined) && - (current.visited === null || current.visited === undefined) - ) { - return min; - } else if (min.visited === null || min.visited === undefined) { - return current; - } else if (current.visited === null || current.visited === undefined) { - return min; - } else if (min.visited === current.visited) { + if (min.visited === current.visited) { return min; } else { - return min.visited < current.visited ? min : current; + // We can safely cast visited to number | string here because we've already + // checked the type at runtime above and we know that it's either a number or a string + return (min.visited as string | number) < + (current.visited as string | number) + ? min + : current; } }, visitedArgs[0]); @@ -395,20 +407,18 @@ class Functions { return { value, index, - visited: visited ? visited : null, + visited, }; }) .sort((a, b) => { - if (a.visited === null && b.visited === null) { - return 0; - } else if (a.visited === null) { - return -1; - } else if (b.visited === null) { - return 1; - } else if (a.visited === b.visited) { + if (a.visited === b.visited) { return a.index - b.index; // Make the sort stable } else { - return a.visited > b.visited ? 1 : -1; + // We can safely cast visited to number | string here because we've already + // checked the type at runtime above and we know that it's either a number or a string + return (a.visited as string | number) > (b.visited as string | number) + ? 1 + : -1; } }) .map(({ value }) => value); // Extract the original values @@ -527,17 +537,34 @@ class Functions { } /** - * TODO: write docs for Functions.signature() - * - * @param options - * @returns + * Decorator to enforce the signature of a function at runtime. + * + * The signature decorator enforces the arity and types of the arguments + * passed to a function at runtime. If the arguments do not match the + * expected arity or types errors are thrown. + * + * @example + * ```typescript + * import { PowertoolsFunctions } from '@aws-lambda-powertools/jmespath/functions'; + * + * class MyFunctions extends Functions { + * ⁣@Functions.signature({ + * argumentsSpecs: [['number'], ['number']], + * variadic: true, + * }) + * public funcMyMethod(args: Array): unknown { + * // ... + * } + * } + * ``` + * + * @param options The options for the signature decorator */ - public static signature(options: SignatureOptions): SignatureDecorator { + public static signature( + options: FunctionSignatureOptions + ): FunctionSignatureDecorator { return (_target, _propertyKey, descriptor) => { const originalMethod = descriptor.value; - if (typeof originalMethod !== 'function') { - throw new TypeError('Only methods can be decorated with @signature.'); - } // Use a function() {} instead of an () => {} arrow function so that we can // access `myClass` as `this` in a decorated `myClass.myMethod()`. diff --git a/packages/jmespath/src/Lexer.ts b/packages/jmespath/src/Lexer.ts index dab1d0e313..d2a4994f70 100644 --- a/packages/jmespath/src/Lexer.ts +++ b/packages/jmespath/src/Lexer.ts @@ -1,13 +1,18 @@ import { + SIMPLE_TOKENS, START_IDENTIFIER, VALID_IDENTIFIER, VALID_NUMBER, WHITESPACE, - SIMPLE_TOKENS, -} from './constants'; -import { LexerError, EmptyExpressionError } from './errors'; -import type { Token } from './types'; +} from './constants.js'; +import { EmptyExpressionError, LexerError } from './errors.js'; +import type { Token } from './types.js'; +/** + * A lexer for JMESPath expressions. + * + * This lexer tokenizes a JMESPath expression into a sequence of tokens. + */ class Lexer { #position!: number; #expression!: string; @@ -15,6 +20,13 @@ class Lexer { #current!: string; #length!: number; + /** + * Tokenize a JMESPath expression. + * + * This method is a generator that yields tokens for the given expression. + * + * @param expression The JMESPath expression to tokenize. + */ public *tokenize(expression: string): Generator { this.#initializeForExpression(expression); while (this.#current !== '' && this.#current !== undefined) { @@ -77,16 +89,12 @@ class Lexer { // Negative number. const start = this.#position; const buff = this.#consumeNumber(); - if (buff.length > 1) { - yield { - type: 'number', - value: parseInt(buff), - start: start, - end: start + buff.length, - }; - } else { - throw new LexerError(start, buff); - } + yield { + type: 'number', + value: parseInt(buff), + start: start, + end: start + buff.length, + }; } else if (this.#current === '"') { yield this.#consumeQuotedIdentifier(); } else if (this.#current === '<') { @@ -105,17 +113,7 @@ class Lexer { }; this.#next(); } else { - let position; - // TODO: check this this.#current === undefined case - if (this.#current === undefined) { - // If we're at the EOF, we never advanced - // the position so we don't need to rewind - // it back one location. - position = this.#position; - } else { - position = this.#position - 1; - } - throw new LexerError(position, '='); + throw new LexerError(this.#position - 1, '='); } } else { throw new LexerError(this.#position, this.#current); @@ -164,8 +162,6 @@ class Lexer { /** * Advance the lexer to the next character in the expression. - * - * @returns The next character in the expression. */ #next(): string { if (this.#position === this.#length - 1) { @@ -196,8 +192,6 @@ class Lexer { if (this.#current === '') { // We've reached the end of the expression (EOF) before // we found the delimiter. This is an error. - // TODO: see if we can pass a message to the Lexer.#consumeUntil() call - // @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/lexer.py#L151 throw new LexerError(start, this.#expression.substring(start)); } buff += this.#current; @@ -210,9 +204,9 @@ class Lexer { } /** - * TODO: write docs for Lexer.#consumeLiteral() + * Process a literal. * - * @returns + * A literal is a JSON string that is enclosed in backticks. */ #consumeLiteral(): Token { const start = this.#position; @@ -227,41 +221,32 @@ class Lexer { end: this.#position - start, }; } catch (error) { - // TODO: see if we can get the error message from JSON.parse() and use that - // @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/lexer.py#L174 throw new LexerError(start, lexeme); } } /** - * TODO: write docs for Lexer.#consumeQuotedIdentifier() + * Process a quoted identifier. * - * @returns + * A quoted identifier is a string that is enclosed in double quotes. */ #consumeQuotedIdentifier(): Token { const start = this.#position; const lexeme = '"' + this.#consumeUntil('"') + '"'; - try { - const tokenLen = this.#position - start; + const tokenLen = this.#position - start; - return { - type: 'quoted_identifier', - value: JSON.parse(lexeme), - start, - end: tokenLen, - }; - } catch (error) { - // TODO: see if we can get the error message from JSON.parse() and use that - // @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/lexer.py#L187 - // const errorMessage = `Invalid quoted identifier: ${lexeme}`; - throw new LexerError(start, lexeme); - } + return { + type: 'quoted_identifier', + value: JSON.parse(lexeme), + start, + end: tokenLen, + }; } /** - * TODO: write docs for Lexer.#consumeRawStringLiteral() + * Process a raw string literal. * - * @returns + * A raw string literal is a string that is enclosed in single quotes. */ #consumeRawStringLiteral(): Token { const start = this.#position; @@ -277,12 +262,11 @@ class Lexer { } /** - * TODO: write docs for Lexer.#matchOrElse() + * Match the expected character and return the corresponding token type. * - * @param expected - * @param matchType - * @param elseType - * @returns + * @param expected The expected character + * @param matchType The token type to return if the expected character is found + * @param elseType The token type to return if the expected character is not found */ #matchOrElse( expected: string, diff --git a/packages/jmespath/src/ParsedResult.ts b/packages/jmespath/src/ParsedResult.ts index 0e5dc3b2ac..30f01e3c62 100644 --- a/packages/jmespath/src/ParsedResult.ts +++ b/packages/jmespath/src/ParsedResult.ts @@ -1,11 +1,12 @@ -import { TreeInterpreter } from './visitor'; +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; +import { TreeInterpreter } from './TreeInterpreter.js'; import { + ArityError, JMESPathTypeError, UnknownFunctionError, - ArityError, VariadicArityError, -} from './errors'; -import type { Node, JSONValue, ParsingOptions } from './types'; +} from './errors.js'; +import type { Node, ParsingOptions } from './types.js'; class ParsedResult { public expression: string; @@ -16,6 +17,12 @@ class ParsedResult { this.parsed = parsed; } + /** + * Perform a JMESPath search on a JSON value. + * + * @param value The JSON value to search + * @param options The parsing options to use + */ public search(value: JSONValue, options?: ParsingOptions): unknown { const interpreter = new TreeInterpreter(options); diff --git a/packages/jmespath/src/Parser.ts b/packages/jmespath/src/Parser.ts index d5bfa85fbc..653b8e30dc 100644 --- a/packages/jmespath/src/Parser.ts +++ b/packages/jmespath/src/Parser.ts @@ -1,33 +1,33 @@ import { randomInt } from 'node:crypto'; -import { BINDING_POWER } from './constants'; +import { Lexer } from './Lexer.js'; +import { ParsedResult } from './ParsedResult.js'; import { + andExpression, + comparator, + currentNode, + expref, field, - literal, - identity, - valueProjection, + filterProjection, flatten, - projection, - notExpression, + functionExpression, + identity, index, - slice, - currentNode, - expref, indexExpression, - comparator, - multiSelectList, - multiSelectDict, keyValPair, - filterProjection, - functionExpression, - pipe, + literal, + multiSelectObject, + multiSelectList, + notExpression, orExpression, - andExpression, + pipe, + projection, + slice, subexpression, -} from './ast'; -import { Lexer } from './Lexer'; -import { ParsedResult } from './ParsedResult'; -import { LexerError, IncompleteExpressionError, ParseError } from './errors'; -import type { Node, Token } from './types'; + valueProjection, +} from './ast.js'; +import { BINDING_POWER } from './constants.js'; +import { IncompleteExpressionError, LexerError, ParseError } from './errors.js'; +import type { Node, Token } from './types.js'; /** * Top down operaotr precedence parser for JMESPath. @@ -59,19 +59,20 @@ class Parser { #maxCacheSize = 128; #tokenizer?: Lexer; #tokens: Token[]; - #bufferSize: number; #index = 0; public constructor(lookahead = 2) { this.#tokens = Array.from({ length: lookahead }); - this.#bufferSize = lookahead; } /** - * TODO: write docs for Parser.parse() + * Parse a JMESPath expression and return the Abstract Syntax Tree (AST) + * that represents the expression. + * + * The AST is cached, so if you parse the same expression multiple times, + * the AST will be returned from the cache. * * @param expression The JMESPath expression to parse. - * @returns The parsed expression. */ public parse(expression: string): ParsedResult { const cached = this.#cache[expression]; @@ -95,10 +96,9 @@ class Parser { } /** - * TODO: write docs for Parser.#doParse() + * Do the actual parsing of the expression. * * @param expression The JMESPath expression to parse. - * @returns The parsed expression. */ #doParse(expression: string): ParsedResult { try { @@ -118,7 +118,7 @@ class Parser { } /** - * TODO: write docs for Parser.#parse() + * Parse a JMESPath expression and return the parsed result. */ #parse(expression: string): ParsedResult { this.#tokenizer = new Lexer(); @@ -138,7 +138,7 @@ class Parser { } /** - * TODO: write docs for Parser.#expression() + * Process an expression. */ #expression(bindingPower = 0): Node { const leftToken = this.#lookaheadToken(0); @@ -155,9 +155,8 @@ class Parser { } /** - * TODO: write docs for arser.#advance() - * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/parser.py#L121-L123 - * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/parser.py#L137-L138 + * Get the nud function for a token. This is the function that + * is called when a token is found at the beginning of an expression. * * @param tokenType The type of token to get the nud function for. */ @@ -282,19 +281,7 @@ class Parser { return andExpression(leftNode, right); } else if (tokenType === 'lparen') { - if (leftNode.type !== 'field' || typeof leftNode.value !== 'string') { - // 0 - first func arg or closing parenthesis - // -1 - '(' token - // -2 - invalid func "name" - const previousToken = this.#lookaheadToken(-2); - throw new ParseError({ - lexPosition: previousToken.start, - tokenValue: previousToken.value, - tokenType: previousToken.type, - reason: `Invalid function name '${previousToken.value}'`, - }); - } - const name = leftNode.value; + const name = leftNode.value as string; const args = []; while (this.#currentToken() !== 'rparen') { const expression = this.#expression(); @@ -358,9 +345,11 @@ class Parser { } /** - * TODO: write docs for Parser.#parseIndexExpression() + * Process an index expression. * - * @returns + * An index expression is a syntax that allows you to + * access elements in a list or dictionary. For example + * `foo[0]` accesses the first element in the list `foo`. */ #parseIndexExpression(): Node { // We're here: @@ -380,9 +369,23 @@ class Parser { } /** - * TODO: write docs for Parser.#parseSliceExpression() + * Process a slice expression. + * + * A slice expression is a syntax that allows you to + * access a range of elements in a list. For example + * `foo[0:10:2]` accesses every other element in the + * list `foo` from index 0 to 10. + * + * In a slice expression, the first index represents the + * start of the slice, the second index represents the + * end of the slice, and the third index represents the + * step. * - * @returns + * If the first index is omitted, it defaults to 0. + * If the second index is omitted, it defaults to the + * length of the list. If the third index is omitted, it + * defaults to 1. If the last colon is omitted, it defaults + * to a single index. */ #parseSliceExpression(): Node { // [start:end:step] @@ -406,10 +409,6 @@ class Parser { } else if (currentToken === 'number') { parts[index] = this.#lookaheadToken(0).value; this.#advance(); - } else if (currentToken === 'current') { - return currentNode(); - } else if (currentToken === 'expref') { - return expref(this.#expression(BINDING_POWER['expref'])); } else { const token = this.#lookaheadToken(0); throw new ParseError({ @@ -426,10 +425,11 @@ class Parser { } /** - * TODO: write docs for Parser.#projectIfSlice() + * Process a projection if the right hand side of the + * projection is a slice. * - * @param left - * @param right + * @param left The left hand side of the projection. + * @param right The right hand side of the projection. */ #projectIfSlice(left: Node, right: Node): Node { const idxExpression = indexExpression([left, right]); @@ -444,11 +444,14 @@ class Parser { } /** - * TODO: write docs for Parser.#parseComparator() - * TODO: narrow comparatorChar type to only values like 'eq', 'ne', etc. + * Process a comparator. * - * @param left - * @param comparatorChar + * A comparator is a syntax that allows you to compare + * two values. For example `foo == bar` compares the + * value of `foo` with the value of `bar`. + * + * @param left The left hand side of the comparator. + * @param comparatorChar The comparator character. */ #parseComparator(left: Node, comparatorChar: Token['type']): Node { return comparator( @@ -459,7 +462,11 @@ class Parser { } /** - * TODO: write docs for Parser.#parseMultiSelectList() + * Process a multi-select list. + * + * A multi-select list is a syntax that allows you to + * select multiple elements from a list. For example + * `foo[*]` selects all elements in the list `foo`. */ #parseMultiSelectList(): Node { const expressions = []; @@ -478,7 +485,12 @@ class Parser { } /** - * TODO: write docs for Parser.#parseMultiSelectHash() + * Process a multi-select hash. + * + * A multi-select hash is a syntax that allows you to + * select multiple key-value pairs from a dictionary. + * For example `foo{a: a, b: b}` selects the keys `a` + * and `b` from the dictionary `foo`. */ #parseMultiSelectHash(): Node { const pairs = []; @@ -500,13 +512,13 @@ class Parser { } } - return multiSelectDict(pairs); + return multiSelectObject(pairs); } /** - * TODO: write docs for Parser.#parseMultiSelectHash() + * Process the right hand side of a projection. * - * @param bindingPower + * @param bindingPower The binding power of the current token. */ #parseProjectionRhs(bindingPower: number): Node { // Parse the right hand side of the projection. @@ -534,9 +546,9 @@ class Parser { } /** - * TODO: write docs for Parser.#parseDotRhs() + * Process the right hand side of a dot expression. * - * @param bindingPower + * @param bindingPower The binding power of the current token. */ #parseDotRhs(bindingPower: number): Node { // From the grammar: @@ -572,9 +584,9 @@ class Parser { } /** - * TODO: write docs for Parser.#match() + * Process a token and throw an error if it doesn't match the expected token. * - * @param tokenType + * @param tokenType The expected token type. */ #match(tokenType: Token['type']): void { const currentToken = this.#currentToken(); @@ -599,9 +611,9 @@ class Parser { } /** - * TODO: write docs for Parser.#matchMultipleTokens() + * Process a token and throw an error if it doesn't match the expected token. * - * @param tokenTypes + * @param tokenTypes The expected token types. */ #matchMultipleTokens(tokenTypes: Token['type'][]): void { const currentToken = this.#currentToken(); @@ -625,32 +637,32 @@ class Parser { } /** - * TODO: write docs for Parser.#advance() + * Advance the index to the next token. */ #advance(): void { this.#index += 1; } /** - * TODO: write docs for Parser.#currentToken() + * Get the current token type. */ #currentToken(): Token['type'] { return this.#tokens[this.#index].type; } /** - * TODO: write docs for Parser.#lookahead() + * Look ahead in the token stream and get the type of the token * - * @param number + * @param number The number of tokens to look ahead. */ #lookahead(number: number): Token['type'] { return this.#tokens[this.#index + number].type; } /** - * TODO: write docs for Parser.#lookaheadToken() + * Look ahead in the token stream and get the token * - * @param number + * @param number The number of tokens to look ahead. */ #lookaheadToken(number: number): Token { return this.#tokens[this.#index + number]; @@ -658,8 +670,6 @@ class Parser { /** * Remove half of the cached expressions randomly. - * - * TODO: check if this is the correct way to do this or maybe replace cache with LRU cache */ #evictCache(): void { const newCache = Object.keys(this.#cache).reduce( diff --git a/packages/jmespath/src/visitor/TreeInterpreter.ts b/packages/jmespath/src/TreeInterpreter.ts similarity index 65% rename from packages/jmespath/src/visitor/TreeInterpreter.ts rename to packages/jmespath/src/TreeInterpreter.ts index ab58766ae5..f13778f1ed 100644 --- a/packages/jmespath/src/visitor/TreeInterpreter.ts +++ b/packages/jmespath/src/TreeInterpreter.ts @@ -1,25 +1,35 @@ -import type { JSONValue, Node, TreeInterpreterOptions } from '../types'; -import { Functions } from '../functions'; +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; import { - Expression, isIntegerNumber, isRecord, isStrictEqual, - isTruthy, - sliceArray, -} from './utils'; +} from '@aws-lambda-powertools/commons/typeutils'; import { ArityError, + JMESPathError, JMESPathTypeError, UnknownFunctionError, VariadicArityError, -} from '../errors'; - +} from './errors.js'; +import { Expression } from './Expression.js'; +import { Functions } from './Functions.js'; +import type { Node, TreeInterpreterOptions } from './types.js'; +import { isTruthy, sliceArray } from './utils.js'; + +/** + * + * A tree interpreter for JMESPath ASTs. + * + * The tree interpreter is responsible for visiting nodes in the AST and + * evaluating them to produce a result. + * + * @internal + */ class TreeInterpreter { #functions: Functions; /** - * @param _options + * @param options The options to use for the interpreter. */ public constructor(options?: TreeInterpreterOptions) { if (options?.customFunctions) { @@ -30,12 +40,12 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visit() - * TODO: finalize types for TreeInterpreter.visit() + * Visit a node in the AST. + * + * The function will call the appropriate method to visit the node based on its type. * - * @param node - * @param value - * @returns + * @param node The node to visit. + * @param value The current value to visit. */ public visit(node: Node, value: JSONValue): JSONValue | undefined { const nodeType = node.type; @@ -48,7 +58,14 @@ class TreeInterpreter { } else if (nodeType === 'current') { return this.#visitCurrent(node, value); } else if (nodeType === 'expref') { - // TODO: review #visitExpref() return type + // This is a special case where we return an instance of the Expression + // class instead of the result of visiting the node. This is because the + // expref node represents a reference to another expression, so we want + // to return an instance of the Expression class so that we can evaluate + // the referenced expression later. Adding `Expression` to the return + // type of the `visit` method would mean having to type-check the return + // type of every call to `visit` in the interpreter even though we only + // return an instance of `Expression` in this one case. // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-next-line return this.#visitExpref(node, value); @@ -70,8 +87,8 @@ class TreeInterpreter { return this.#visitKeyValPair(node, value); } else if (nodeType === 'literal') { return this.#visitLiteral(node, value); - } else if (nodeType === 'multi_select_dict') { - return this.#visitMultiSelectDict(node, value); + } else if (nodeType === 'multi_select_object') { + return this.#visitMultiSelectObject(node, value); } else if (nodeType === 'multi_select_list') { return this.#visitMultiSelectList(node, value); } else if (nodeType === 'or_expression') { @@ -87,16 +104,17 @@ class TreeInterpreter { } else if (nodeType === 'value_projection') { return this.#visitValueProjection(node, value); } else { - // TODO: convert to a custom error - throw new Error(`Not Implemented: Invalid node type: ${node.type}`); + throw new JMESPathError( + `Not Implemented: Invalid node type: ${node.type}` + ); } } /** - * TODO: write docs for TreeInterpreter.visitSubexpression() - * @param node - * @param value - * @returns + * Visit a subexpression node. + * + * @param node The subexpression node to visit. + * @param value The current value to visit. */ #visitSubexpression(node: Node, value: JSONValue): JSONValue { let result = value; @@ -108,10 +126,10 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visitField() - * @param node - * @param value - * @returns + * Visit a field node. + * + * @param node The field node to visit. + * @param value The current value to visit. */ #visitField(node: Node, value: JSONValue): JSONValue { if (!node.value) return null; @@ -127,10 +145,10 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visitComparator() - * @param node - * @param value - * @returns + * Visit a comparator node. + * + * @param node The comparator node to visit. + * @param value The current value to visit. */ #visitComparator(node: Node, value: JSONValue): JSONValue { const comparator = node.value; @@ -157,40 +175,37 @@ class TreeInterpreter { } else { return left >= right; } - } else { - return null; } } else { - // TODO: make invalid comparator a custom error - throw new Error(`Invalid comparator: ${comparator}`); + throw new JMESPathError(`Invalid comparator: ${comparator}`); } } /** - * TODO: write docs for TreeInterpreter.visitCurrent() - * @param node - * @param value - * @returns + * Visit a current node. + * + * @param node The current node to visit. + * @param value The current value to visit. */ #visitCurrent(_node: Node, value: JSONValue): JSONValue { return value; } /** - * TODO: write docs for TreeInterpreter.visitExpref() - * @param node - * @param value - * @returns + * Visit an expref node. + * + * @param node The expref node to visit. + * @param value The current value to visit. */ #visitExpref(node: Node, _value: JSONValue): Expression { return new Expression(node.children[0], this); } /** - * TODO: write docs for TreeInterpreter.visitFunctionExpression() - * @param node - * @param value - * @returns + * Visit a function expression node. + * + * @param node The function expression node to visit. + * @param value The current value to visit. */ #visitFunctionExpression(node: Node, value: JSONValue): JSONValue { const args = []; @@ -199,11 +214,23 @@ class TreeInterpreter { } // check that method name is a string if (typeof node.value !== 'string') { - throw new Error(`Function name must be a string, got ${node.value}`); - } - const methods = Object.getOwnPropertyNames( - Object.getPrototypeOf(this.#functions) - ); + throw new JMESPathError( + `Function name must be a string, got ${node.value}` + ); + } + // get all methods of the functions object + const functionsProto = Object.getPrototypeOf(this.#functions); + const methods = [ + ...Object.getOwnPropertyNames(functionsProto), + // If the functions object's prototype is the Functions class, then it + // must be a custom functions object, so we'll also include the methods + // from the Functions class itself. + ...(functionsProto.__proto__.constructor.name === 'Functions' + ? Object.getOwnPropertyNames( + Object.getPrototypeOf(this.#functions).__proto__ + ) + : []), + ]; // convert snake_case to camelCase const normalizedFunctionName = node.value.replace(/_([a-z])/g, (g) => g[1].toUpperCase() @@ -229,8 +256,8 @@ class TreeInterpreter { } catch (error) { if ( error instanceof JMESPathTypeError || - error instanceof ArityError || - error instanceof VariadicArityError + error instanceof VariadicArityError || + error instanceof ArityError ) { error.setFunctionName(node.value); throw error; @@ -239,10 +266,10 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visitFilterProjection() - * @param node - * @param value - * @returns + * Visit a filter projection node. + * + * @param node The filter projection node to visit. + * @param value The current value to visit. */ #visitFilterProjection(node: Node, value: JSONValue): JSONValue { const base = this.visit(node.children[0], value); @@ -264,10 +291,10 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visitFlatten() - * @param node - * @param value - * @returns + * Visit a flatten node. + * + * @param node The flatten node to visit. + * @param value The current value to visit. */ #visitFlatten(node: Node, value: JSONValue): JSONValue { const base = this.visit(node.children[0], value); @@ -287,29 +314,29 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visitIdentity() - * @param node - * @param value - * @returns + * Visit an identity node. + * + * @param node The identity node to visit. + * @param value The current value to visit. */ #visitIdentity(_node: Node, value: JSONValue): JSONValue { return value; } /** - * TODO: write docs for TreeInterpreter.visitIndex() - * @param node - * @param value - * @returns + * Visit an index node. + * + * @param node The index node to visit. + * @param value The current value to visit. */ #visitIndex(node: Node, value: JSONValue): JSONValue { - // The Python implementation doesn't support string indexing - // even though we could, so we won't either for now. if (!Array.isArray(value)) { return null; } + // The Python implementation doesn't support string indexing + // even though we could, so we won't either for now. if (typeof node.value !== 'number') { - throw new Error(`Invalid index: ${node.value}`); + throw new JMESPathError(`Invalid index: ${node.value}`); } const index = node.value < 0 ? value.length + node.value : node.value; const found = value[index]; @@ -321,10 +348,10 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visitIndexExpression() - * @param node - * @param value - * @returns + * Visit an index expression node. + * + * @param node The index expression node to visit. + * @param value The current value to visit. */ #visitIndexExpression(node: Node, value: JSONValue): JSONValue { let result = value; @@ -336,10 +363,10 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visitSlice() - * @param node - * @param value - * @returns + * Visit a slice node. + * + * @param node The slice node to visit. + * @param value The current value to visit. */ #visitSlice(node: Node, value: JSONValue): JSONValue { const step = isIntegerNumber(node.children[2]) ? node.children[2] : 1; @@ -353,41 +380,41 @@ class TreeInterpreter { return []; } - return sliceArray( - value, - node.children[0] as unknown as number, - node.children[1] as unknown as number, - step - ); + return sliceArray({ + array: value, + start: node.children[0] as unknown as number, + end: node.children[1] as unknown as number, + step, + }); } /** - * TODO: write docs for TreeInterpreter.visitKeyValPair() - * @param node - * @param value - * @returns + * Visit a key-value pair node. + * + * @param node The key-value pair node to visit. + * @param value The current value to visit. */ #visitKeyValPair(node: Node, value: JSONValue): JSONValue { return this.visit(node.children[0], value); } /** - * TODO: write docs for TreeInterpreter.visitLiteral() - * @param node - * @param value - * @returns + * Visit a literal node. + * + * @param node The literal node to visit. + * @param value The current value to visit. */ #visitLiteral(node: Node, _value: JSONValue): JSONValue { return node.value; } /** - * TODO: write docs for TreeInterpreter.visitMultiSelectDict() - * @param node - * @param value - * @returns + * Visit a multi-select object node. + * + * @param node The multi-select object node to visit. + * @param value The current value to visit. */ - #visitMultiSelectDict(node: Node, value: JSONValue): JSONValue { + #visitMultiSelectObject(node: Node, value: JSONValue): JSONValue { if (Object.is(value, null)) { return null; } @@ -402,10 +429,10 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visitMultiSelectList() - * @param node - * @param value - * @returns + * Visit a multi-select list node. + * + * @param node The multi-select list node to visit. + * @param value The current value to visit. */ #visitMultiSelectList(node: Node, value: JSONValue): JSONValue { if (Object.is(value, null)) { @@ -420,10 +447,10 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visitOrExpression() - * @param node - * @param value - * @returns + * Visit an or expression node. + * + * @param node The or expression node to visit. + * @param value The current value to visit. */ #visitOrExpression(node: Node, value: JSONValue): JSONValue { const matched = this.visit(node.children[0], value); @@ -435,10 +462,10 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visitAndExpression() - * @param node - * @param value - * @returns + * Visit an and expression node. + * + * @param node The and expression node to visit. + * @param value The current value to visit. */ #visitAndExpression(node: Node, value: JSONValue): JSONValue { const matched = this.visit(node.children[0], value); @@ -450,10 +477,10 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visitNotExpression() - * @param node - * @param value - * @returns + * Visit a not expression node. + * + * @param node The not expression node to visit. + * @param value The current value to visit. */ #visitNotExpression(node: Node, value: JSONValue): JSONValue { const originalResult = this.visit(node.children[0], value); @@ -467,10 +494,10 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visitPipe() - * @param node - * @param value - * @returns + * Visit a pipe node. + * + * @param node The pipe node to visit. + * @param value The current value to visit. */ #visitPipe(node: Node, value: JSONValue): JSONValue { let result = value; @@ -482,10 +509,10 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visitProjection() - * @param node - * @param value - * @returns + * Visit a projection node. + * + * @param node The projection node to visit. + * @param value The current value to visit. */ #visitProjection(node: Node, value: JSONValue): JSONValue { const base = this.visit(node.children[0], value); @@ -504,10 +531,10 @@ class TreeInterpreter { } /** - * TODO: write docs for TreeInterpreter.visitValueProjection() - * @param node - * @param value - * @returns + * Visit a value projection node. + * + * @param node The value projection node to visit. + * @param value The current value to visit. */ #visitValueProjection(node: Node, value: JSONValue): JSONValue { const base = this.visit(node.children[0], value); diff --git a/packages/jmespath/src/ast.ts b/packages/jmespath/src/ast.ts index 5769f4238d..c0f2eff39f 100644 --- a/packages/jmespath/src/ast.ts +++ b/packages/jmespath/src/ast.ts @@ -1,7 +1,14 @@ -import type { JSONValue, Node } from './types'; +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; +import type { Node } from './types.js'; /** - * TODO: write docs for comparator() + * AST node representing a comparator expression. + * + * A comparator expression is a binary expression that compares two values. + * + * @param name The name of the comparator + * @param first The left-hand side of the comparator + * @param second The right-hand side of the comparator */ const comparator = (name: string, first: Node, second: Node): Node => ({ type: 'comparator', @@ -10,7 +17,10 @@ const comparator = (name: string, first: Node, second: Node): Node => ({ }); /** - * TODO: write docs for currentNode() + * AST node representing the current node. + * + * The current node is a reference to the current value being processed. + * In JMESPath, the current node is represented by the `@` symbol. */ const currentNode = (): Node => ({ type: 'current', @@ -18,7 +28,12 @@ const currentNode = (): Node => ({ }); /** - * TODO: write docs for expref() + * AST node representing an expression reference. + * + * An expression reference is a reference to another expression. + * In JMESPath, an expression reference is represented by the `&` symbol. + * + * @param expression The expression to reference */ const expref = (expression: Node): Node => ({ type: 'expref', @@ -26,7 +41,16 @@ const expref = (expression: Node): Node => ({ }); /** - * TODO: write docs for functionExpression() + * AST node representing a function expression. + * + * A function expression is a reference to a function and its arguments. + * The JMESPath specification defines a set of built-in functions that can + * be used in expressions like `length(@)`, `map(@, &foo)`, etc. + * + * Custom functions can be added by extending the `Functions` class. + * + * @param name The name of the function + * @param args The arguments to the function */ const functionExpression = (name: string, args: Node[]): Node => ({ type: 'function_expression', @@ -35,7 +59,9 @@ const functionExpression = (name: string, args: Node[]): Node => ({ }); /** - * TODO: write docs for field() + * AST node representing a field reference. + * + * A field reference is a reference to a field in an object. */ const field = (name: JSONValue): Node => ({ type: 'field', @@ -44,12 +70,18 @@ const field = (name: JSONValue): Node => ({ }); /** - * TODO: write docs for fieldExpression() + * AST node representing a filter projection. * - * @param left - * @param right - * @param comparator - * @returns + * A filter projection is a binary expression that filters the left-hand side + * based on the right-hand side. + * + * In JMESPath, a filter projection is represented by the `[]` operator. + * For example, `people[?age > 18]` filters the `people` array based on the + * `age` field. + * + * @param left The left-hand side of the filter projection + * @param right The right-hand side of the filter projection + * @param comparator The comparator to use for the filter */ const filterProjection = (left: Node, right: Node, comparator: Node): Node => ({ type: 'filter_projection', @@ -57,12 +89,16 @@ const filterProjection = (left: Node, right: Node, comparator: Node): Node => ({ }); /** - * TODO: write docs for flatten() + * AST node representing a flatten expression. + * + * A flatten expression is a unary expression that flattens an array of arrays + * into a single array. + * + * In JMESPath, a flatten expression is represented by the `[]` operator. + * For example, `people[].name` flattens the `people` array and returns the + * `name` field of each object in the array. * - * @param left - * @param right - * @param comparator - * @returns + * @param node The node to flatten */ const flatten = (node: Node): Node => ({ type: 'flatten', @@ -70,22 +106,17 @@ const flatten = (node: Node): Node => ({ }); /** - * TODO: write docs for identity() - * - * @param left - * @param right - * @param comparator - * @returns + * AST node representing an identity expression. */ const identity = (): Node => ({ type: 'identity', children: [] }); /** - * TODO: write docs for index() + * AST node representing an index reference. + * + * An index reference is a reference to an index in an array. + * For example, `people[0]` references the first element in the `people` array. * - * @param left - * @param right - * @param comparator - * @returns + * @param index The index to reference */ const index = (index: JSONValue): Node => ({ type: 'index', @@ -94,12 +125,11 @@ const index = (index: JSONValue): Node => ({ }); /** - * TODO: write docs for indexExpression() + * AST node representing an index expression. * - * @param left - * @param right - * @param comparator - * @returns + * An index expression holds the index and the children of the expression. + * + * @param children The children of the index expression */ const indexExpression = (children: Node[]): Node => ({ type: 'index_expression', @@ -107,10 +137,10 @@ const indexExpression = (children: Node[]): Node => ({ }); /** - * TODO: write docs for keyValPair() + * AST node representing a key-value pair. * - * @param keyName - * @param node + * @param keyName The name of the key + * @param node The value of the key */ const keyValPair = (keyName: JSONValue, node: Node): Node => ({ type: 'key_val_pair', @@ -119,9 +149,11 @@ const keyValPair = (keyName: JSONValue, node: Node): Node => ({ }); /** - * TODO: write docs for literal() + * AST node representing a literal value. + * + * A literal value is a value that is not a reference to another node. * - * @param literalValue + * @param literalValue The value of the literal */ const literal = (literalValue: JSONValue): Node => ({ type: 'literal', @@ -130,19 +162,19 @@ const literal = (literalValue: JSONValue): Node => ({ }); /** - * TODO: write docs for multiSelectDict() - * TODO: check if multiSelectDict() could be possibly be renamed to multiSelectObject() / multiSelectMap() / multiSelectHash() + * AST node representing a multi-select object. + * + * A multi-select object is a reference to multiple nodes in an object. * * @param nodes */ -const multiSelectDict = (nodes: Node[]): Node => ({ - type: 'multi_select_dict', +const multiSelectObject = (nodes: Node[]): Node => ({ + type: 'multi_select_object', children: nodes, }); /** - * TODO: write docs for multiSelectList() - * TODO: check if multiSelectList() could be possibly be renamed to multiSelectArray() + * AST node representing a multi-select list. * * @param nodes */ @@ -152,9 +184,10 @@ const multiSelectList = (nodes: Node[]): Node => ({ }); /** - * TODO: write docs for orExpression() - * @param left - * @param right + * AST node representing an or expression. + * + * @param left The left-hand side of the or expression + * @param right The right-hand side of the or expression */ const orExpression = (left: Node, right: Node): Node => ({ type: 'or_expression', @@ -162,9 +195,10 @@ const orExpression = (left: Node, right: Node): Node => ({ }); /** - * TODO: write docs for andExpression() - * @param left - * @param right + * AST node representing an and expression. + * + * @param left The left-hand side of the and expression + * @param right The right-hand side of the and expression */ const andExpression = (left: Node, right: Node): Node => ({ type: 'and_expression', @@ -172,9 +206,10 @@ const andExpression = (left: Node, right: Node): Node => ({ }); /** - * TODO: write docs for notExpression() - * @param left - * @param right + * AST node representing a not expression. + * + * @param left The left-hand side of the not expression + * @param right The right-hand side of the not expression */ const notExpression = (expr: Node): Node => ({ type: 'not_expression', @@ -182,9 +217,10 @@ const notExpression = (expr: Node): Node => ({ }); /** - * TODO: write docs for multiSelectList() - * @param left - * @param right + * AST node representing a pipe expression. + * + * @param left The left-hand side of the pipe expression + * @param right The right-hand side of the pipe expression */ const pipe = (left: Node, right: Node): Node => ({ type: 'pipe', @@ -192,9 +228,10 @@ const pipe = (left: Node, right: Node): Node => ({ }); /** - * TODO: write docs for projection() - * @param left - * @param right + * AST node representing a projection. + * + * @param left The left-hand side of the projection + * @param right The right-hand side of the projection */ const projection = (left: Node, right: Node): Node => ({ type: 'projection', @@ -202,8 +239,9 @@ const projection = (left: Node, right: Node): Node => ({ }); /** - * TODO: write docs for subexpression() - * @param children + * AST node representing a subexpression. + * + * @param children The children of the subexpression */ const subexpression = (children: Node[]): Node => ({ type: 'subexpression', @@ -211,12 +249,13 @@ const subexpression = (children: Node[]): Node => ({ }); /** - * TODO: write docs for slice() - * TODO: fix type for slice() + * AST node representing a slice. + * + * A slice is a reference to a range of values in an array. * - * @param start - * @param end - * @param step + * @param start The start of the slice + * @param end The end of the slice + * @param step The step of the slice */ const slice = (start: JSONValue, end: JSONValue, step: JSONValue): Node => ({ type: 'slice', @@ -224,10 +263,10 @@ const slice = (start: JSONValue, end: JSONValue, step: JSONValue): Node => ({ }); /** - * TODO: write docs for valueProjection() + * AST node representing a value projection. * - * @param left - * @param right + * @param left The left-hand side of the value projection + * @param right The right-hand side of the value projection */ const valueProjection = (left: Node, right: Node): Node => ({ type: 'value_projection', @@ -235,26 +274,26 @@ const valueProjection = (left: Node, right: Node): Node => ({ }); export { + andExpression, comparator, currentNode, expref, - functionExpression, field, filterProjection, flatten, + functionExpression, identity, index, indexExpression, keyValPair, literal, - multiSelectDict, + multiSelectObject, multiSelectList, - orExpression, - andExpression, notExpression, + orExpression, pipe, projection, - subexpression, slice, + subexpression, valueProjection, }; diff --git a/packages/jmespath/src/constants.ts b/packages/jmespath/src/constants.ts index 0dd6436957..642e3eb3f9 100644 --- a/packages/jmespath/src/constants.ts +++ b/packages/jmespath/src/constants.ts @@ -1,3 +1,10 @@ +/** + * The binding powers for the various tokens in the JMESPath grammar. + * + * The binding powers are used to determine the order of operations for + * the parser. The higher the binding power, the more tightly the token + * binds to its arguments. + */ const BINDING_POWER = { eof: 0, unquoted_identifier: 0, @@ -31,15 +38,41 @@ const BINDING_POWER = { lparen: 60, } as const; +/** + * The set of ASCII lowercase letters allowed in JMESPath identifiers. + */ const ASCII_LOWERCASE = 'abcdefghijklmnopqrstuvwxyz'; +/** + * The set of ASCII uppercase letters allowed in JMESPath identifiers. + */ const ASCII_UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +/** + * The set of ASCII letters allowed in JMESPath identifiers. + */ const ASCII_LETTERS = ASCII_LOWERCASE + ASCII_UPPERCASE; +/** + * The set of ASCII digits allowed in JMESPath identifiers. + */ const DIGITS = '0123456789'; - +/** + * The set of ASCII letters and digits allowed in JMESPath identifiers. + */ const START_IDENTIFIER = new Set(ASCII_LETTERS + '_'); +/** + * The set of ASCII letters and digits allowed in JMESPath identifiers. + */ const VALID_IDENTIFIER = new Set(ASCII_LETTERS + DIGITS + '_'); +/** + * The set of ASCII digits allowed in JMESPath identifiers. + */ const VALID_NUMBER = new Set(DIGITS); +/** + * The set of ASCII whitespace characters allowed in JMESPath identifiers. + */ const WHITESPACE = new Set(' \t\n\r'); +/** + * The set of simple tokens in the JMESPath grammar. + */ const SIMPLE_TOKENS: Map = new Map([ ['.', 'dot'], ['*', 'star'], @@ -56,9 +89,9 @@ const SIMPLE_TOKENS: Map = new Map([ export { BINDING_POWER, - WHITESPACE, + SIMPLE_TOKENS, START_IDENTIFIER, VALID_IDENTIFIER, VALID_NUMBER, - SIMPLE_TOKENS, + WHITESPACE, }; diff --git a/packages/jmespath/src/errors.ts b/packages/jmespath/src/errors.ts index 0f878cd6a1..4d56fd96aa 100644 --- a/packages/jmespath/src/errors.ts +++ b/packages/jmespath/src/errors.ts @@ -1,7 +1,7 @@ -import type { Token } from './types'; +import type { Token } from './types.js'; /** - * TODO: write docs for JMESPathError + * Base class for errors thrown during expression parsing and evaluation. */ class JMESPathError extends Error { /** @@ -35,7 +35,6 @@ class JMESPathError extends Error { /** * Error thrown when an unknown token is encountered during the AST construction. - * TODO: improve field names for LexerError */ class LexerError extends JMESPathError { /** @@ -60,7 +59,6 @@ class LexerError extends JMESPathError { /** * Error thrown when an invalid or unexpected token type or value is encountered during parsing. - * TODO: improve field names for ParseError */ class ParseError extends JMESPathError { /** @@ -104,11 +102,7 @@ class ParseError extends JMESPathError { } /** - * TODO: complete IncompleteExpressionError implementation - * TODO: write docs for IncompleteExpressionError - * TODO: add `name` to `IncompleteExpressionError` - * - * @see https://github.com/jmespath/jmespath.py/blob/develop/jmespath/exceptions.py#L32 + * Error thrown when an incomplete expression is encountered during parsing. */ class IncompleteExpressionError extends ParseError { /** @@ -117,11 +111,20 @@ class IncompleteExpressionError extends ParseError { * Can be set by whatever catches the error. */ public expression?: string; + + public constructor(options: { + lexPosition: number; + tokenValue: Token['value']; + tokenType: Token['type']; + reason?: string; + }) { + super(options); + this.name = 'IncompleteExpressionError'; + } } /** - * TODO: write docs for EmptyExpressionError - * TODO: see if this is ever being thrown + * Error thrown when an empty expression is encountered during parsing. */ class EmptyExpressionError extends JMESPathError { public constructor() { @@ -209,7 +212,14 @@ class ArityError extends FunctionError { } /** - * TODO: write docs for VariadicArityError + * Error thrown when an unexpected number of arguments is passed to a variadic function. + * + * Variadic functions are functions that accept a variable number of arguments. + * For example, the `max()` function accepts any number of arguments and returns + * the largest one. If no arguments are passed, it returns `null`. + * + * If the number of arguments passed to a variadic function is not within the + * expected range, this error is thrown. */ class VariadicArityError extends ArityError { public constructor(options: { expectedArity: number; actualArity: number }) { @@ -281,13 +291,13 @@ class UnknownFunctionError extends FunctionError { } export { + ArityError, + EmptyExpressionError, + IncompleteExpressionError, JMESPathError, + JMESPathTypeError, LexerError, ParseError, - IncompleteExpressionError, - ArityError, - VariadicArityError, - JMESPathTypeError, - EmptyExpressionError, UnknownFunctionError, + VariadicArityError, }; diff --git a/packages/jmespath/src/functions/index.ts b/packages/jmespath/src/functions/index.ts deleted file mode 100644 index 049f75632b..0000000000 --- a/packages/jmespath/src/functions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Functions'; diff --git a/packages/jmespath/src/functions/typeChecking.ts b/packages/jmespath/src/functions/typeChecking.ts deleted file mode 100644 index fac4f88ddf..0000000000 --- a/packages/jmespath/src/functions/typeChecking.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Expression, getType, isRecord } from '../visitor/utils'; -import { JMESPathTypeError, ArityError, VariadicArityError } from '../errors'; - -/** - * TODO: write docs for arityCheck() - * - * @param args - * @param argumentsSpecs - * @param decoratedFuncName - * @param variadic - */ -const arityCheck = ( - args: unknown[], - argumentsSpecs: Array>, - variadic?: boolean -): void => { - if (variadic) { - if (args.length < argumentsSpecs.length) { - throw new VariadicArityError({ - expectedArity: argumentsSpecs.length, - actualArity: args.length, - }); - } - } else if (args.length !== argumentsSpecs.length) { - throw new ArityError({ - expectedArity: argumentsSpecs.length, - actualArity: args.length, - }); - } -}; - -/** - * TODO: write docs for typeCheck() - * @param args - * @param argumentsSpecs - */ -const typeCheck = ( - args: unknown[], - argumentsSpecs: Array> -): void => { - argumentsSpecs.forEach((argumentSpec, index) => { - typeCheckArgument(args[index], argumentSpec); - }); -}; - -/** - * Type checks an argument against a list of types. - * - * Type checking at runtime involves checking the top level type, - * and in the case of arrays, potentially checking the types of - * the elements in the array. - * - * If the list of types includes 'any', then the type check is a - * no-op. - * - * If the list of types includes more than one type, then the - * argument is checked against each type in the list. If the - * argument matches any of the types, then the type check - * passes. If the argument does not match any of the types, then - * a JMESPathTypeError is thrown. - * - * @param arg - * @param argumentSpec - */ -const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { - if (argumentSpec.length === 0 || argumentSpec[0] === 'any') { - return; - } - const entryCount = argumentSpec.length; - let hasMoreTypesToCheck = argumentSpec.length > 1; - for (const [index, type] of argumentSpec.entries()) { - hasMoreTypesToCheck = index < entryCount - 1; - if (type.startsWith('array')) { - if (!Array.isArray(arg)) { - if (hasMoreTypesToCheck) { - continue; - } - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - if (type.includes('-')) { - const arrayItemsType = type.slice(6); - let actualType: string | undefined; - for (const element of arg) { - try { - typeCheckArgument(element, [arrayItemsType]); - actualType = arrayItemsType; - } catch (error) { - if (!hasMoreTypesToCheck || actualType !== undefined) { - throw error; - } - } - } - } - break; - } else { - if (type === 'expression') { - if (!(arg instanceof Expression)) { - if (!hasMoreTypesToCheck) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - continue; - } - break; - } else if (type === 'string' || type === 'number' || type === 'boolean') { - if (typeof arg !== type) { - if (!hasMoreTypesToCheck) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - continue; - } - break; - } else if (type === 'null') { - if (!Object.is(arg, null)) { - if (!hasMoreTypesToCheck) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - continue; - } - break; - } else if (type === 'object') { - if (!isRecord(arg)) { - if (index === entryCount - 1) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - continue; - } - break; - } - } - } -}; - -export { arityCheck, typeCheck }; diff --git a/packages/jmespath/src/index.ts b/packages/jmespath/src/index.ts index 5a2bdeb5b8..a0f2545f85 100644 --- a/packages/jmespath/src/index.ts +++ b/packages/jmespath/src/index.ts @@ -1 +1,12 @@ -export * from './search'; +export { search } from './search.js'; +export { + JMESPathError, + LexerError, + ParseError, + IncompleteExpressionError, + ArityError, + VariadicArityError, + JMESPathTypeError, + EmptyExpressionError, + UnknownFunctionError, +} from './errors.js'; diff --git a/packages/jmespath/src/search.ts b/packages/jmespath/src/search.ts index 7492a8b901..536a463bf3 100644 --- a/packages/jmespath/src/search.ts +++ b/packages/jmespath/src/search.ts @@ -1,20 +1,61 @@ -import { Parser } from './Parser'; -import type { JSONValue, ParsingOptions } from './types'; +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; +import { Parser } from './Parser.js'; +import type { ParsingOptions } from './types.js'; + +const parser = new Parser(); /** - * TODO: write docs for search() + * Search for data in a JSON object using a JMESPath expression. + * + * @example + * ```typescript + * import { search } from '@aws-lambda-powertools/jmespath'; + * + * const data = { + * foo: { + * bar: { + * baz: 1 + * } + * } + * }; + * + * const result = search('foo.bar.baz', data); + * console.log(result); // 1 + * ``` + * + * By default the search function will use all the built-in functions + * present in the [JMESPath specification](https://jmespath.org/specification.html). + * + * Powertools for AWS Lambda provides some additional functions that can be used + * by passing them in the `customFunctions` option. + * + * @example + * ```typescript + * import { search } from '@aws-lambda-powertools/jmespath'; + * import { PowertoolsFunctions } from '@aws-lambda-powertools/jmespath/functions'; + * + * const data = { + * body: "{\"foo\": \"bar\"}" + * }; + * + * const result = search( + * 'powertools_json(body)', + * data, + * { customFunctions: new PowertoolsFunctions() } + * ); + * console.log(result); // { foo: 'bar' } + * ``` * - * @param expression - * @param data - * @param options - * @returns + * @param expression The JMESPath expression to use + * @param data The JSON object to search + * @param options The parsing options to use */ const search = ( expression: string, data: JSONValue, options?: ParsingOptions ): unknown => { - return new Parser().parse(expression).search(data, options); + return parser.parse(expression).search(data, options); }; export { search }; diff --git a/packages/jmespath/src/types.ts b/packages/jmespath/src/types.ts new file mode 100644 index 0000000000..7ea771e9ba --- /dev/null +++ b/packages/jmespath/src/types.ts @@ -0,0 +1,101 @@ +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; +import type { Functions } from './Functions.js'; +import { BINDING_POWER } from './constants.js'; + +/** + * A token in the JMESPath AST. + */ +type Token = { + type: keyof typeof BINDING_POWER; + value: JSONValue; + start: number; + end: number; +}; + +/** + * A node in the JMESPath AST. + */ +type Node = { + type: string; + children: Node[]; + value?: JSONValue; +}; + +/** + * Options for the tree interpreter. + */ +type TreeInterpreterOptions = { + /** + * The custom functions to use. + * + * By default, the interpreter uses the standard JMESPath functions + * available in the [JMESPath specification](https://jmespath.org/specification.html). + */ + customFunctions?: Functions; +}; + +/** + * Options for parsing. + * + * You can use this type to customize the parsing of JMESPath expressions. + * + * For example, you can use this type to provide custom functions to the parser. + * + * @example + * ```typescript + * import { search } from '@aws-lambda-powertools/jmespath'; + * import { PowertoolsFunctions } from '@aws-lambda-powertools/jmespath/functions'; + * + * const expression = 'powertools_json(@)'; + * + * const result = search(expression, "{\n \"a\": 1\n}", { + * customFunctions: new PowertoolsFunctions(), + * }); + * console.log(result); // { a: 1 } + * ``` + */ +type ParsingOptions = TreeInterpreterOptions; + +/** + * Decorator for function signatures. + */ +type FunctionSignatureDecorator = ( + target: Functions | typeof Functions, + propertyKey: string | symbol, + descriptor: PropertyDescriptor +) => void; + +/** + * Options for a function signature. + * + * @example + * ```typescript + * import { PowertoolsFunctions } from '@aws-lambda-powertools/jmespath/functions'; + * + * class MyFunctions extends Functions { + * ⁣@Functions.signature({ + * argumentsSpecs: [['number'], ['number']], + * variadic: true, + * }) + * public funcMyMethod(args: Array): unknown { + * // ... + * } + * } + * ``` + * + * @param argumentsSpecs The expected arguments for the function. + * @param variadic Whether the function is variadic. + */ +type FunctionSignatureOptions = { + argumentsSpecs: Array>; + variadic?: boolean; +}; + +export type { + FunctionSignatureDecorator, + FunctionSignatureOptions, + Node, + ParsingOptions, + Token, + TreeInterpreterOptions, +}; diff --git a/packages/jmespath/src/types/AST.ts b/packages/jmespath/src/types/AST.ts deleted file mode 100644 index b54c449fcc..0000000000 --- a/packages/jmespath/src/types/AST.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { JSONValue } from './JSON'; - -/** - * TODO: write docs for Node type - */ -type Node = { - type: string; - children: Node[]; - value?: JSONValue; -}; - -export { Node }; diff --git a/packages/jmespath/src/types/JSON.ts b/packages/jmespath/src/types/JSON.ts deleted file mode 100644 index 2a44a5ba99..0000000000 --- a/packages/jmespath/src/types/JSON.ts +++ /dev/null @@ -1,6 +0,0 @@ -type JSONPrimitive = string | number | boolean | null | undefined; -type JSONValue = JSONPrimitive | JSONObject | JSONArray; -type JSONObject = { [key: string]: JSONValue }; -type JSONArray = Array; - -export { JSONPrimitive, JSONValue, JSONObject, JSONArray }; diff --git a/packages/jmespath/src/types/ParsedResult.ts b/packages/jmespath/src/types/ParsedResult.ts deleted file mode 100644 index f7ff92c435..0000000000 --- a/packages/jmespath/src/types/ParsedResult.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { TreeInterpreterOptions } from './TreeInterpreter'; - -/** - * TODO: write docs for ParsingOptions type - */ -type ParsingOptions = TreeInterpreterOptions; - -export type { ParsingOptions }; diff --git a/packages/jmespath/src/types/Token.ts b/packages/jmespath/src/types/Token.ts deleted file mode 100644 index 4dd5f3fcc7..0000000000 --- a/packages/jmespath/src/types/Token.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { BINDING_POWER } from '../constants'; -import type { JSONValue } from './JSON'; - -/** - * TODO: write docs for Token type - */ -type Token = { - type: keyof typeof BINDING_POWER; - value: JSONValue; - start: number; - end: number; -}; - -export { Token }; diff --git a/packages/jmespath/src/types/TreeInterpreter.ts b/packages/jmespath/src/types/TreeInterpreter.ts deleted file mode 100644 index aaa9fc6583..0000000000 --- a/packages/jmespath/src/types/TreeInterpreter.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Functions } from '../functions'; - -/** - * TODO: write docs for TreeInterpreterOptions type - */ -type TreeInterpreterOptions = { customFunctions?: Functions }; - -export { TreeInterpreterOptions }; diff --git a/packages/jmespath/src/types/index.ts b/packages/jmespath/src/types/index.ts deleted file mode 100644 index dc9bb68082..0000000000 --- a/packages/jmespath/src/types/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './Token'; -export * from './JSON'; -export * from './AST'; -export * from './TreeInterpreter'; -export * from './ParsedResult'; diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts new file mode 100644 index 0000000000..329f66bfc7 --- /dev/null +++ b/packages/jmespath/src/utils.ts @@ -0,0 +1,257 @@ +import { + getType, + isIntegerNumber, + isRecord, + isTruthy as isTruthyJS, + isNumber, +} from '@aws-lambda-powertools/commons/typeutils'; +import { Expression } from './Expression.js'; +import { ArityError, JMESPathTypeError, VariadicArityError } from './errors.js'; + +/** + * Check if a value is truthy. + * + * In JavaScript, zero is falsy while all other non-zero numbers are truthy. + * In JMESPath however, zero is truthy as well as all other non-zero numbers. For + * this reason we wrap the original isTruthy function from the commons package + * and add a check for numbers. + * + * @param value The value to check + */ +const isTruthy = (value: unknown): boolean => { + if (isNumber(value)) { + return true; + } else { + return isTruthyJS(value); + } +}; + +/** + * @internal + * Cap a slice range value to the length of an array, taking into account + * negative values and whether the step is negative. + * + * @param arrayLength The length of the array + * @param value The value to cap + * @param isStepNegative Whether the step is negative + */ +const capSliceRange = ( + arrayLength: number, + value: number, + isStepNegative: boolean +): number => { + if (value < 0) { + value += arrayLength; + if (value < 0) { + value = isStepNegative ? -1 : 0; + } + } else if (value >= arrayLength) { + value = isStepNegative ? arrayLength - 1 : arrayLength; + } + + return value; +}; + +/** + * Given a start, stop, and step value, the sub elements in an array are extracted as follows: + * * The first element in the extracted array is the index denoted by start. + * * The last element in the extracted array is the index denoted by end - 1. + * * The step value determines how many indices to skip after each element is selected from the array. An array of 1 (the default step) will not skip any indices. A step value of 2 will skip every other index while extracting elements from an array. A step value of -1 will extract values in reverse order from the array. + * + * Slice expressions adhere to the following rules: + * * If a negative start position is given, it is calculated as the total length of the array plus the given start position. + * * If no start position is given, it is assumed to be 0 if the given step is greater than 0 or the end of the array if the given step is less than 0. + * * If a negative stop position is given, it is calculated as the total length of the array plus the given stop position. + * * If no stop position is given, it is assumed to be the length of the array if the given step is greater than 0 or 0 if the given step is less than 0. + * * If the given step is omitted, it it assumed to be 1. + * * If the given step is 0, an invalid-value error MUST be raised (thrown before calling the function) + * * If the element being sliced is not an array, the result is null (returned before calling the function) + * * If the element being sliced is an array and yields no results, the result MUST be an empty array. + * + * @param array The array to slice + * @param start The start index + * @param end The end index + * @param step The step value + */ +const sliceArray = ({ + array, + start, + end, + step, +}: { + array: T[]; + start?: number; + end?: number; + step: number; +}): T[] | null => { + const isStepNegative = step < 0; + const length = array.length; + + start = isIntegerNumber(start) + ? capSliceRange(length, start, isStepNegative) + : isStepNegative + ? length - 1 + : 0; + + end = isIntegerNumber(end) + ? capSliceRange(length, end, isStepNegative) + : isStepNegative + ? -1 + : length; + + const result: T[] = []; + if (step > 0) { + for (let i = start; i < end; i += step) { + result.push(array[i]); + } + } else { + for (let i = start; i > end; i += step) { + result.push(array[i]); + } + } + + return result; +}; + +/** + * Checks if the number of arguments passed to a function matches the expected arity. + * If the number of arguments does not match the expected arity, an ArityError is thrown. + * + * If the function is variadic, then the number of arguments passed to the function must be + * greater than or equal to the expected arity. If the number of arguments passed to the function + * is less than the expected arity, a `VariadicArityError` is thrown. + * + * @param args The arguments passed to the function + * @param argumentsSpecs The expected types for each argument + * @param decoratedFuncName The name of the function being called + * @param variadic Whether the function is variadic + */ +const arityCheck = ( + args: unknown[], + argumentsSpecs: Array>, + variadic?: boolean +): void => { + if (variadic) { + if (args.length < argumentsSpecs.length) { + throw new VariadicArityError({ + expectedArity: argumentsSpecs.length, + actualArity: args.length, + }); + } + } else if (args.length !== argumentsSpecs.length) { + throw new ArityError({ + expectedArity: argumentsSpecs.length, + actualArity: args.length, + }); + } +}; + +/** + * Type checks the arguments passed to a function against the expected types. + * + * @param args The arguments passed to the function + * @param argumentsSpecs The expected types for each argument + */ +const typeCheck = ( + args: unknown[], + argumentsSpecs: Array> +): void => { + argumentsSpecs.forEach((argumentSpec, index) => { + typeCheckArgument(args[index], argumentSpec); + }); +}; + +/** + * Type checks an argument against a list of types. + * + * Type checking at runtime involves checking the top level type, + * and in the case of arrays, potentially checking the types of + * the elements in the array. + * + * If the list of types includes 'any', then the type check is a + * no-op. + * + * If the list of types includes more than one type, then the + * argument is checked against each type in the list. If the + * argument matches any of the types, then the type check + * passes. If the argument does not match any of the types, then + * a JMESPathTypeError is thrown. + * + * @param arg + * @param argumentSpec + */ +const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { + if (argumentSpec.length === 0 || argumentSpec[0] === 'any') { + return; + } + const entryCount = argumentSpec.length; + let hasMoreTypesToCheck = argumentSpec.length > 1; + for (const [index, type] of argumentSpec.entries()) { + hasMoreTypesToCheck = index < entryCount - 1; + if (type.startsWith('array')) { + if (!Array.isArray(arg)) { + if (hasMoreTypesToCheck) { + continue; + } + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); + } + if (type.includes('-')) { + const arrayItemsType = type.slice(6); + let actualType: string | undefined; + for (const element of arg) { + try { + typeCheckArgument(element, [arrayItemsType]); + actualType = arrayItemsType; + } catch (error) { + if (!hasMoreTypesToCheck || actualType !== undefined) { + throw error; + } + } + } + } + break; + } else { + if (type === 'expression') { + if (!(arg instanceof Expression)) { + if (!hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); + } + } + break; + } else if (type === 'string' || type === 'number' || type === 'boolean') { + if (typeof arg !== type) { + if (!hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); + } + continue; + } + break; + } else if (type === 'object') { + if (!isRecord(arg)) { + if (index === entryCount - 1) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); + } + } + break; + } + } + } +}; + +export { isTruthy, arityCheck, sliceArray, typeCheck, typeCheckArgument }; diff --git a/packages/jmespath/src/visitor/index.ts b/packages/jmespath/src/visitor/index.ts deleted file mode 100644 index 943851fc79..0000000000 --- a/packages/jmespath/src/visitor/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './TreeInterpreter'; diff --git a/packages/jmespath/src/visitor/utils.ts b/packages/jmespath/src/visitor/utils.ts deleted file mode 100644 index 6c2c882037..0000000000 --- a/packages/jmespath/src/visitor/utils.ts +++ /dev/null @@ -1,235 +0,0 @@ -import type { TreeInterpreter } from './TreeInterpreter'; -import type { Node, JSONValue } from '../types'; - -/** - * Apply a JMESPath expression to a JSON value. - */ -class Expression { - readonly #expression: Node; - readonly #interpreter: TreeInterpreter; - - public constructor(expression: Node, interpreter: TreeInterpreter) { - this.#expression = expression; - this.#interpreter = interpreter; - } - - public visit(value: JSONValue, node?: Node): JSONValue { - return this.#interpreter.visit(node ?? this.#expression, value); - } -} - -/** - * TODO: write docs for isRecord() type guard - * - * @param value - * @returns - */ -const isRecord = (value: unknown): value is Record => { - return ( - Object.prototype.toString.call(value) === '[object Object]' && - !Object.is(value, null) - ); -}; - -/** - * TODO: write docs for isTruthy() - * @param value - * @returns - */ -const isTruthy = (value: unknown): boolean => { - if (typeof value === 'string') { - return value !== ''; - } else if (typeof value === 'number') { - return true; - } else if (typeof value === 'boolean') { - return value; - } else if (Array.isArray(value)) { - return value.length > 0; - } else if (isRecord(value)) { - return Object.keys(value).length > 0; - } else { - return Object.is(value, true); - } -}; - -/** - * Check if two unknown values are strictly equal. - * - * If the values are arrays, then each element is compared, regardless of - * order. If the values are objects, then each key and value from left - * is compared to the corresponding key and value from right. If the - * values are primitives, then they are compared using strict equality. - * - * @param left Left side of strict equality comparison - * @param right Right side of strict equality comparison - * @returns True if the values are strictly equal, false otherwise - */ -const isStrictEqual = (left: unknown, right: unknown): boolean => { - if (left === right) { - return true; - } else if (typeof left !== typeof right) { - return false; - } else if (Array.isArray(left) && Array.isArray(right)) { - if (left.length !== right.length) { - return false; - } - for (const [i, value] of left.entries()) { - if (!isStrictEqual(value, right[i])) { - return false; - } - } - - return true; - } else if (isRecord(left) && isRecord(right)) { - const leftKeys = Object.keys(left); - const leftValues = Object.values(left); - const rightKeys = Object.keys(right); - const rightValues = Object.values(right); - if ( - leftKeys.length !== rightKeys.length || - leftValues.length !== rightValues.length - ) { - return false; - } - - return ( - isStrictEqual(leftKeys, rightKeys) && - isStrictEqual(leftValues, rightValues) - ); - } else { - return false; - } -}; - -/** - * Check if a value is a number. - * - * @param value The value to check - * @returns True if the value is a number, false otherwise - */ -const isNumber = (value: unknown): value is number => { - return typeof value === 'number'; -}; - -/** - * Check if a value is an integer number. - * - * @param value The value to check - * @returns True if the value is an integer number, false otherwise - */ -const isIntegerNumber = (value: unknown): value is number => { - return isNumber(value) && Number.isInteger(value); -}; - -/** - * @internal - * Cap a slice range value to the length of an array, taking into account - * negative values and whether the step is negative. - * - * @param arrayLength The length of the array - * @param value The value to cap - * @param isStepNegative Whether the step is negative - * @returns The capped value - */ -const capSliceRange = ( - arrayLength: number, - value: number, - isStepNegative: boolean -): number => { - if (value < 0) { - value += arrayLength; - if (value < 0) { - value = isStepNegative ? -1 : 0; - } - } else if (value >= arrayLength) { - value = isStepNegative ? arrayLength - 1 : arrayLength; - } - - return value; -}; - -/** - * Given a start, stop, and step value, the sub elements in an array are extracted as follows: - * * The first element in the extracted array is the index denoted by start. - * * The last element in the extracted array is the index denoted by end - 1. - * * The step value determines how many indices to skip after each element is selected from the array. An array of 1 (the default step) will not skip any indices. A step value of 2 will skip every other index while extracting elements from an array. A step value of -1 will extract values in reverse order from the array. - * - * Slice expressions adhere to the following rules: - * * If a negative start position is given, it is calculated as the total length of the array plus the given start position. - * * If no start position is given, it is assumed to be 0 if the given step is greater than 0 or the end of the array if the given step is less than 0. - * * If a negative stop position is given, it is calculated as the total length of the array plus the given stop position. - * * If no stop position is given, it is assumed to be the length of the array if the given step is greater than 0 or 0 if the given step is less than 0. - * * If the given step is omitted, it it assumed to be 1. - * * If the given step is 0, an invalid-value error MUST be raised (thrown before calling the function) - * * If the element being sliced is not an array, the result is null (returned before calling the function) - * * If the element being sliced is an array and yields no results, the result MUST be an empty array. - * - * @param array The array to slice - * @param start The start index - * @param end The end index - * @param step The step value - */ -const sliceArray = ( - array: T[], - start?: number, - end?: number, - step?: number -): T[] | null => { - step = isIntegerNumber(step) ? step : 1; - const isStepNegative = step < 0; - const length = array.length; - - start = isIntegerNumber(start) - ? capSliceRange(length, start, isStepNegative) - : isStepNegative - ? length - 1 - : 0; - - end = isIntegerNumber(end) - ? capSliceRange(length, end, isStepNegative) - : isStepNegative - ? -1 - : length; - - const result: T[] = []; - if (step > 0) { - for (let i = start; i < end; i += step) { - result.push(array[i]); - } - } else { - for (let i = start; i > end; i += step) { - result.push(array[i]); - } - } - - return result; -}; - -const getType = (value: unknown): string => { - if (Array.isArray(value)) { - return 'array'; - } else if (isRecord(value)) { - return 'object'; - } else if (typeof value === 'string') { - return 'string'; - } else if (isNumber(value)) { - return 'number'; - } else if (typeof value === 'boolean') { - return 'boolean'; - } else if (Object.is(value, null)) { - return 'null'; - } else { - return 'unknown'; - } -}; - -export { - Expression, - isRecord, - isTruthy, - isStrictEqual, - isNumber, - isIntegerNumber, - sliceArray, - getType, -}; diff --git a/packages/jmespath/tests/unit/base.test.ts b/packages/jmespath/tests/unit/compliance/base.test.ts similarity index 98% rename from packages/jmespath/tests/unit/base.test.ts rename to packages/jmespath/tests/unit/compliance/base.test.ts index a64655f88f..d0b7b72114 100644 --- a/packages/jmespath/tests/unit/base.test.ts +++ b/packages/jmespath/tests/unit/compliance/base.test.ts @@ -3,7 +3,7 @@ * * @group unit/jmespath/compliance/base */ -import { search } from '../../src'; +import { search } from '../../../src'; describe('Base tests', () => { it.each([ diff --git a/packages/jmespath/tests/unit/boolean.test.ts b/packages/jmespath/tests/unit/compliance/boolean.test.ts similarity index 99% rename from packages/jmespath/tests/unit/boolean.test.ts rename to packages/jmespath/tests/unit/compliance/boolean.test.ts index ea0fe8f749..ae91b16f4e 100644 --- a/packages/jmespath/tests/unit/boolean.test.ts +++ b/packages/jmespath/tests/unit/compliance/boolean.test.ts @@ -3,7 +3,7 @@ * * @group unit/jmespath/compliance/boolean */ -import { search } from '../../src'; +import { search } from '../../../src'; describe('Boolean tests', () => { it.each([ diff --git a/packages/jmespath/tests/unit/current.test.ts b/packages/jmespath/tests/unit/compliance/current.test.ts similarity index 95% rename from packages/jmespath/tests/unit/current.test.ts rename to packages/jmespath/tests/unit/compliance/current.test.ts index 29aebd0985..273b8fb43a 100644 --- a/packages/jmespath/tests/unit/current.test.ts +++ b/packages/jmespath/tests/unit/compliance/current.test.ts @@ -3,7 +3,7 @@ * * @group unit/jmespath/compliance/current */ -import { search } from '../../src'; +import { search } from '../../../src'; describe('Current operator tests', () => { it.each([ diff --git a/packages/jmespath/tests/unit/escape.test.ts b/packages/jmespath/tests/unit/compliance/escape.test.ts similarity index 97% rename from packages/jmespath/tests/unit/escape.test.ts rename to packages/jmespath/tests/unit/compliance/escape.test.ts index a20cea2a05..259d4fcae5 100644 --- a/packages/jmespath/tests/unit/escape.test.ts +++ b/packages/jmespath/tests/unit/compliance/escape.test.ts @@ -3,7 +3,7 @@ * * @group unit/jmespath/compliance/escape */ -import { search } from '../../src'; +import { search } from '../../../src'; describe('Escape characters tests', () => { it.each([ diff --git a/packages/jmespath/tests/unit/filters.test.ts b/packages/jmespath/tests/unit/compliance/filters.test.ts similarity index 99% rename from packages/jmespath/tests/unit/filters.test.ts rename to packages/jmespath/tests/unit/compliance/filters.test.ts index aa14b9f6a7..a85ee40a2a 100644 --- a/packages/jmespath/tests/unit/filters.test.ts +++ b/packages/jmespath/tests/unit/compliance/filters.test.ts @@ -3,7 +3,7 @@ * * @group unit/jmespath/compliance/filters */ -import { search } from '../../src'; +import { search } from '../../../src'; describe('Filer operator tests', () => { it.each([ diff --git a/packages/jmespath/tests/unit/functions.test.ts b/packages/jmespath/tests/unit/compliance/functions.test.ts similarity index 99% rename from packages/jmespath/tests/unit/functions.test.ts rename to packages/jmespath/tests/unit/compliance/functions.test.ts index 013d24f60a..eb77d30d03 100644 --- a/packages/jmespath/tests/unit/functions.test.ts +++ b/packages/jmespath/tests/unit/compliance/functions.test.ts @@ -3,7 +3,7 @@ * * @group unit/jmespath/compliance/functions */ -import { search } from '../../src'; +import { search } from '../../../src'; describe('Functions tests', () => { it.each([ diff --git a/packages/jmespath/tests/unit/identifiers.test.ts b/packages/jmespath/tests/unit/compliance/identifiers.test.ts similarity index 99% rename from packages/jmespath/tests/unit/identifiers.test.ts rename to packages/jmespath/tests/unit/compliance/identifiers.test.ts index d1e19b8142..9b33997bc6 100644 --- a/packages/jmespath/tests/unit/identifiers.test.ts +++ b/packages/jmespath/tests/unit/compliance/identifiers.test.ts @@ -3,7 +3,7 @@ * * @group unit/jmespath/compliance/identifiers */ -import { search } from '../../src'; +import { search } from '../../../src'; describe('Identifiers tests', () => { it.each([ diff --git a/packages/jmespath/tests/unit/indices.test.ts b/packages/jmespath/tests/unit/compliance/indices.test.ts similarity index 99% rename from packages/jmespath/tests/unit/indices.test.ts rename to packages/jmespath/tests/unit/compliance/indices.test.ts index c02718b594..75353adf7e 100644 --- a/packages/jmespath/tests/unit/indices.test.ts +++ b/packages/jmespath/tests/unit/compliance/indices.test.ts @@ -3,7 +3,7 @@ * * @group unit/jmespath/compliance/indices */ -import { search } from '../../src'; +import { search } from '../../../src'; describe('Indices tests', () => { it.each([ diff --git a/packages/jmespath/tests/unit/literal.test.ts b/packages/jmespath/tests/unit/compliance/literal.test.ts similarity index 99% rename from packages/jmespath/tests/unit/literal.test.ts rename to packages/jmespath/tests/unit/compliance/literal.test.ts index ffc68d6cb8..465702c4be 100644 --- a/packages/jmespath/tests/unit/literal.test.ts +++ b/packages/jmespath/tests/unit/compliance/literal.test.ts @@ -3,7 +3,7 @@ * * @group unit/jmespath/compliance/literal */ -import { search } from '../../src'; +import { search } from '../../../src'; describe('Literal expressions tests', () => { it.each([ diff --git a/packages/jmespath/tests/unit/multiselect.test.ts b/packages/jmespath/tests/unit/compliance/multiselect.test.ts similarity index 99% rename from packages/jmespath/tests/unit/multiselect.test.ts rename to packages/jmespath/tests/unit/compliance/multiselect.test.ts index 4d2697fb1a..eddf2adc56 100644 --- a/packages/jmespath/tests/unit/multiselect.test.ts +++ b/packages/jmespath/tests/unit/compliance/multiselect.test.ts @@ -3,7 +3,7 @@ * * @group unit/jmespath/compliance/multiselect */ -import { search } from '../../src'; +import { search } from '../../../src'; describe('Multiselect expressions tests', () => { it.each([ diff --git a/packages/jmespath/tests/unit/pipe.test.ts b/packages/jmespath/tests/unit/compliance/pipe.test.ts similarity index 98% rename from packages/jmespath/tests/unit/pipe.test.ts rename to packages/jmespath/tests/unit/compliance/pipe.test.ts index 943e0b0f21..752b5895dc 100644 --- a/packages/jmespath/tests/unit/pipe.test.ts +++ b/packages/jmespath/tests/unit/compliance/pipe.test.ts @@ -3,7 +3,7 @@ * * @group unit/jmespath/compliance/pipe */ -import { search } from '../../src'; +import { search } from '../../../src'; describe('Pipe expressions tests', () => { it.each([ diff --git a/packages/jmespath/tests/unit/slice.test.ts b/packages/jmespath/tests/unit/compliance/slice.test.ts similarity index 96% rename from packages/jmespath/tests/unit/slice.test.ts rename to packages/jmespath/tests/unit/compliance/slice.test.ts index ce639f7678..bc797d88b2 100644 --- a/packages/jmespath/tests/unit/slice.test.ts +++ b/packages/jmespath/tests/unit/compliance/slice.test.ts @@ -3,7 +3,7 @@ * * @group unit/jmespath/compliance/slice */ -import { search } from '../../src'; +import { search } from '../../../src'; describe('Slices tests', () => { it.each([ @@ -147,7 +147,7 @@ describe('Slices tests', () => { { expression: 'foo[8:2&]', error: - 'Invalid jmespath expression: parse error at column 8, found unexpected token "]" (rbracket) in expression: foo[8:2&]', + 'Invalid jmespath expression: parse error at column 7, found unexpected token "&" (expref) in expression: foo[8:2&]', }, { expression: 'foo[2:a:3]', diff --git a/packages/jmespath/tests/unit/syntax.test.ts b/packages/jmespath/tests/unit/compliance/syntax.test.ts similarity index 99% rename from packages/jmespath/tests/unit/syntax.test.ts rename to packages/jmespath/tests/unit/compliance/syntax.test.ts index c38f8afc72..a27203504b 100644 --- a/packages/jmespath/tests/unit/syntax.test.ts +++ b/packages/jmespath/tests/unit/compliance/syntax.test.ts @@ -3,7 +3,7 @@ * * @group unit/jmespath/compliance/syntax */ -import { search } from '../../src'; +import { search } from '../../../src'; describe('Syntax tests', () => { it.each([ diff --git a/packages/jmespath/tests/unit/unicode.test.ts b/packages/jmespath/tests/unit/compliance/unicode.test.ts similarity index 97% rename from packages/jmespath/tests/unit/unicode.test.ts rename to packages/jmespath/tests/unit/compliance/unicode.test.ts index 8a841dabdd..4f1f201dc6 100644 --- a/packages/jmespath/tests/unit/unicode.test.ts +++ b/packages/jmespath/tests/unit/compliance/unicode.test.ts @@ -3,7 +3,7 @@ * * @group unit/jmespath/compliance/unicode */ -import { search } from '../../src'; +import { search } from '../../../src'; describe('Unicode tests', () => { it.each([ diff --git a/packages/jmespath/tests/unit/wildcard.test.ts b/packages/jmespath/tests/unit/compliance/wildcard.test.ts similarity index 99% rename from packages/jmespath/tests/unit/wildcard.test.ts rename to packages/jmespath/tests/unit/compliance/wildcard.test.ts index 8fabbd3077..d8e8a478e7 100644 --- a/packages/jmespath/tests/unit/wildcard.test.ts +++ b/packages/jmespath/tests/unit/compliance/wildcard.test.ts @@ -3,7 +3,7 @@ * * @group unit/jmespath/compliance/wildcard */ -import { search } from '../../src'; +import { search } from '../../../src'; describe('Wildcard tests', () => { it.each([ diff --git a/packages/jmespath/tests/unit/index.test.ts b/packages/jmespath/tests/unit/index.test.ts index 12be4673f8..896b23459f 100644 --- a/packages/jmespath/tests/unit/index.test.ts +++ b/packages/jmespath/tests/unit/index.test.ts @@ -1,22 +1,256 @@ /** * Test Compliance with the JMESPath specification * - * @group unit/jmespath/compliance/canary + * @group unit/jmespath/coverage */ -import { search } from '../../src'; +import { + search, + EmptyExpressionError, + ArityError, + LexerError, + JMESPathError, + VariadicArityError, +} from '../../src'; +import { Functions } from '../../src/Functions.js'; +import { Parser } from '../../src/Parser.js'; +import { TreeInterpreter } from '../../src/TreeInterpreter.js'; -describe('index', () => { - it('should be defined', () => { - const expression = 'foo[8:2:0]'; - const data = { - type: 'object', - }; +describe('Coverage tests', () => { + // These expressions tests are not part of the compliance suite, but are added to ensure coverage + describe('expressions', () => { + it('throws an error if the expression is not a string', () => { + // Prepare + const notAStringExpression = 3; - /* const res = search(expression, data); - expect(res).toEqual([1, 2]); */ + // Act & Assess + expect(() => + search(notAStringExpression as unknown as string, {}) + ).toThrow(EmptyExpressionError); + }); - expect(() => search(expression, data)).toThrowError( - 'Invalid slice, step cannot be 0' - ); + it('throws a lexer error when encounteirng a single equal for equality', () => { + // Prepare + const expression = '='; + + // Act & Assess + expect(() => { + search(expression, {}); + }).toThrow(LexerError); + }); + + it('returns null when max_by is called with an empty list', () => { + // Prepare + const expression = 'max_by(@, &foo)'; + + // Act + const result = search(expression, []); + + // Assess + expect(result).toBe(null); + }); + + it('returns null when min_by is called with an empty list', () => { + // Prepare + const expression = 'min_by(@, &foo)'; + + // Act + const result = search(expression, []); + + // Assess + expect(result).toBe(null); + }); + + it('returns the correct max value', () => { + // Prepare + const expression = 'max(@)'; + + // Act + const result = search(expression, ['z', 'b']); + + // Assess + expect(result).toBe('z'); + }); + + it('returns the correct min value', () => { + // Prepare + const expression = 'min(@)'; + + // Act + const result = search(expression, ['z', 'b']); + + // Assess + expect(result).toBe('b'); + }); + }); + + describe('type checking', () => { + class TestFunctions extends Functions { + @TestFunctions.signature({ + argumentsSpecs: [['any'], ['any']], + }) + public funcTest(): void { + return; + } + + @TestFunctions.signature({ + argumentsSpecs: [['any'], ['any']], + variadic: true, + }) + public funcTestArityError(): void { + return; + } + } + + it('throws an arity error if the function is called with the wrong number of arguments', () => { + // Prepare + const expression = 'test(@, @, @)'; + + // Act & Assess + expect(() => + search(expression, {}, { customFunctions: new TestFunctions() }) + ).toThrow(ArityError); + }); + + it('throws an arity error if the function is called with the wrong number of arguments', () => { + // Prepare + const expression = 'test_arity_error(@)'; + + // Act & Assess + expect(() => + search(expression, {}, { customFunctions: new TestFunctions() }) + ).toThrow(VariadicArityError); + }); + }); + + describe('class: Parser', () => { + it('clears the cache when purgeCache is called', () => { + // Prepare + const parser = new Parser(); + + // Act + const parsedResultA = parser.parse('test(@, @)'); + parser.purgeCache(); + const parsedResultB = parser.parse('test(@, @)'); + + // Assess + expect(parsedResultA).not.toBe(parsedResultB); + }); + }); + + describe('class: TreeInterpreter', () => { + it('throws an error when visiting an invalid node', () => { + // Prepare + const interpreter = new TreeInterpreter(); + + // Act & Assess + expect(() => { + interpreter.visit( + { + type: 'invalid', + value: 'invalid', + children: [], + }, + {} + ); + }).toThrow(JMESPathError); + }); + + it('returns null when visiting a field with no value', () => { + // Prepare + const interpreter = new TreeInterpreter(); + + // Act + const result = interpreter.visit( + { + type: 'field', + value: undefined, + children: [], + }, + {} + ); + + // Assess + expect(result).toBe(null); + }); + + it('throws an error when receiving an invalid comparator', () => { + // Prepare + const interpreter = new TreeInterpreter(); + + // Act & Assess + expect(() => { + interpreter.visit( + { + type: 'comparator', + value: 'invalid', + children: [ + { + type: 'field', + value: 'a', + children: [], + }, + { + type: 'field', + value: 'b', + children: [], + }, + ], + }, + {} + ); + }).toThrow(JMESPathError); + }); + + it('throws an error when receiving a function with an invalid name', () => { + // Prepare + const interpreter = new TreeInterpreter(); + + // Act & Assess + expect(() => { + interpreter.visit( + { + type: 'function_expression', + value: 1, // function name must be a string + children: [], + }, + {} + ); + }).toThrow(JMESPathError); + }); + + it('throws an error when receiving an index expression with an invalid index', () => { + // Prepare + const interpreter = new TreeInterpreter(); + + // Act & Assess + expect(() => { + interpreter.visit( + { + type: 'index', + value: 'invalid', // index must be a number + children: [], + }, + [] + ); + }).toThrow(JMESPathError); + }); + + it('returns an empty array when slicing an empty array', () => { + // Prepare + const interpreter = new TreeInterpreter(); + + // Act + const result = interpreter.visit( + { + type: 'slice', + value: [0, 0, 1], + children: [], + }, + [] + ); + + // Assess + expect(result).toEqual([]); + }); }); }); diff --git a/packages/jmespath/tsconfig.esm.json b/packages/jmespath/tsconfig.esm.json new file mode 100644 index 0000000000..123291b0cf --- /dev/null +++ b/packages/jmespath/tsconfig.esm.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.esm.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "./lib/esm", + "rootDir": "./src", + "tsBuildInfoFile": ".tsbuildinfo/esm.json" + }, + "include": [ + "./src/**/*" + ] +} \ No newline at end of file diff --git a/packages/jmespath/tsconfig.json b/packages/jmespath/tsconfig.json index 67b7b1e01e..0f7ea73e2a 100644 --- a/packages/jmespath/tsconfig.json +++ b/packages/jmespath/tsconfig.json @@ -1,39 +1,11 @@ { + "extends": "../../tsconfig.json", "compilerOptions": { - "experimentalDecorators": true, - "noImplicitAny": true, - "target": "ES2020", - "module": "commonjs", - "declaration": true, - "declarationMap": true, - "outDir": "lib", - "removeComments": false, - "strict": true, - "inlineSourceMap": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "pretty": true, - "baseUrl": "src/", - "rootDirs": [ - "src/" - ], - "esModuleInterop": true + "outDir": "./lib/cjs/", + "rootDir": "./src", + "tsBuildInfoFile": ".tsbuildinfo/cjs.json" }, "include": [ - "src/**/*" - ], - "exclude": [ - "./node_modules" - ], - "watchOptions": { - "watchFile": "useFsEvents", - "watchDirectory": "useFsEvents", - "fallbackPolling": "dynamicPriority" - }, - "lib": [ - "es2020" - ], - "types": [ - "node" + "./src/**/*" ] } \ No newline at end of file diff --git a/packages/jmespath/typedoc.json b/packages/jmespath/typedoc.json new file mode 100644 index 0000000000..879b1d55e7 --- /dev/null +++ b/packages/jmespath/typedoc.json @@ -0,0 +1,7 @@ +{ + "extends": ["../../typedoc.base.json"], + "entryPoints": [ + "./src/index.ts" + ], + "readme": "README.md" +} \ No newline at end of file From 1c2032c19889833fb4ed499cf5da069528bb3a84 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 1 Mar 2024 18:40:11 +0100 Subject: [PATCH 074/103] chore: update types to include object --- packages/jmespath/src/Expression.ts | 5 +-- packages/jmespath/src/Functions.ts | 8 +++- packages/jmespath/src/ParsedResult.ts | 5 +-- packages/jmespath/src/TreeInterpreter.ts | 55 ++++++++++++------------ packages/jmespath/src/search.ts | 5 +-- packages/jmespath/src/types.ts | 8 +++- 6 files changed, 46 insertions(+), 40 deletions(-) diff --git a/packages/jmespath/src/Expression.ts b/packages/jmespath/src/Expression.ts index 3efb27f680..94c69ef3c4 100644 --- a/packages/jmespath/src/Expression.ts +++ b/packages/jmespath/src/Expression.ts @@ -1,6 +1,5 @@ -import type { JSONValue } from '@aws-lambda-powertools/commons/types'; import type { TreeInterpreter } from './TreeInterpreter.js'; -import type { Node } from './types.js'; +import type { JSONObject, Node } from './types.js'; /** * Apply a JMESPath expression to a JSON value. @@ -21,7 +20,7 @@ class Expression { * @param node The node to visit. * @returns The result of applying the expression to the value. */ - public visit(value: JSONValue, node?: Node): JSONValue { + public visit(value: JSONObject, node?: Node): JSONObject { return this.#interpreter.visit(node ?? this.#expression, value); } } diff --git a/packages/jmespath/src/Functions.ts b/packages/jmespath/src/Functions.ts index 27c66bbb23..c8f6ec72e4 100644 --- a/packages/jmespath/src/Functions.ts +++ b/packages/jmespath/src/Functions.ts @@ -13,6 +13,7 @@ import { JMESPathTypeError } from './errors.js'; import type { FunctionSignatureDecorator, FunctionSignatureOptions, + JSONObject as JSONObjectType, } from './types.js'; import { arityCheck, typeCheck } from './utils.js'; @@ -176,8 +177,11 @@ class Functions { @Functions.signature({ argumentsSpecs: [['any'], ['array']], }) - public funcMap(expression: Expression, args: JSONArray): JSONArray { - return args.map((arg: JSONValue) => { + public funcMap( + expression: Expression, + args: JSONArray + ): JSONArray | Array { + return args.map((arg: JSONObjectType) => { return expression.visit(arg) || null; }); } diff --git a/packages/jmespath/src/ParsedResult.ts b/packages/jmespath/src/ParsedResult.ts index 30f01e3c62..63610bff31 100644 --- a/packages/jmespath/src/ParsedResult.ts +++ b/packages/jmespath/src/ParsedResult.ts @@ -1,4 +1,3 @@ -import type { JSONValue } from '@aws-lambda-powertools/commons/types'; import { TreeInterpreter } from './TreeInterpreter.js'; import { ArityError, @@ -6,7 +5,7 @@ import { UnknownFunctionError, VariadicArityError, } from './errors.js'; -import type { Node, ParsingOptions } from './types.js'; +import type { Node, ParsingOptions, JSONObject } from './types.js'; class ParsedResult { public expression: string; @@ -23,7 +22,7 @@ class ParsedResult { * @param value The JSON value to search * @param options The parsing options to use */ - public search(value: JSONValue, options?: ParsingOptions): unknown { + public search(value: JSONObject, options?: ParsingOptions): unknown { const interpreter = new TreeInterpreter(options); try { diff --git a/packages/jmespath/src/TreeInterpreter.ts b/packages/jmespath/src/TreeInterpreter.ts index f13778f1ed..8030f559f8 100644 --- a/packages/jmespath/src/TreeInterpreter.ts +++ b/packages/jmespath/src/TreeInterpreter.ts @@ -1,4 +1,3 @@ -import type { JSONValue } from '@aws-lambda-powertools/commons/types'; import { isIntegerNumber, isRecord, @@ -13,7 +12,7 @@ import { } from './errors.js'; import { Expression } from './Expression.js'; import { Functions } from './Functions.js'; -import type { Node, TreeInterpreterOptions } from './types.js'; +import type { Node, TreeInterpreterOptions, JSONObject } from './types.js'; import { isTruthy, sliceArray } from './utils.js'; /** @@ -47,7 +46,7 @@ class TreeInterpreter { * @param node The node to visit. * @param value The current value to visit. */ - public visit(node: Node, value: JSONValue): JSONValue | undefined { + public visit(node: Node, value: JSONObject): JSONObject | null { const nodeType = node.type; if (nodeType === 'subexpression') { return this.#visitSubexpression(node, value); @@ -116,7 +115,7 @@ class TreeInterpreter { * @param node The subexpression node to visit. * @param value The current value to visit. */ - #visitSubexpression(node: Node, value: JSONValue): JSONValue { + #visitSubexpression(node: Node, value: JSONObject): JSONObject { let result = value; for (const child of node.children) { result = this.visit(child, result); @@ -131,14 +130,14 @@ class TreeInterpreter { * @param node The field node to visit. * @param value The current value to visit. */ - #visitField(node: Node, value: JSONValue): JSONValue { + #visitField(node: Node, value: JSONObject): JSONObject { if (!node.value) return null; if ( isRecord(value) && typeof node.value === 'string' && node.value in value ) { - return value[node.value]; + return value[node.value] as JSONObject; } else { return null; } @@ -150,7 +149,7 @@ class TreeInterpreter { * @param node The comparator node to visit. * @param value The current value to visit. */ - #visitComparator(node: Node, value: JSONValue): JSONValue { + #visitComparator(node: Node, value: JSONObject): JSONObject { const comparator = node.value; const left = this.visit(node.children[0], value); const right = this.visit(node.children[1], value); @@ -187,7 +186,7 @@ class TreeInterpreter { * @param node The current node to visit. * @param value The current value to visit. */ - #visitCurrent(_node: Node, value: JSONValue): JSONValue { + #visitCurrent(_node: Node, value: JSONObject): JSONObject { return value; } @@ -197,7 +196,7 @@ class TreeInterpreter { * @param node The expref node to visit. * @param value The current value to visit. */ - #visitExpref(node: Node, _value: JSONValue): Expression { + #visitExpref(node: Node, _value: JSONObject): Expression { return new Expression(node.children[0], this); } @@ -207,7 +206,7 @@ class TreeInterpreter { * @param node The function expression node to visit. * @param value The current value to visit. */ - #visitFunctionExpression(node: Node, value: JSONValue): JSONValue { + #visitFunctionExpression(node: Node, value: JSONObject): JSONObject { const args = []; for (const child of node.children) { args.push(this.visit(child, value)); @@ -271,7 +270,7 @@ class TreeInterpreter { * @param node The filter projection node to visit. * @param value The current value to visit. */ - #visitFilterProjection(node: Node, value: JSONValue): JSONValue { + #visitFilterProjection(node: Node, value: JSONObject): JSONObject { const base = this.visit(node.children[0], value); if (!Array.isArray(base)) { return null; @@ -296,7 +295,7 @@ class TreeInterpreter { * @param node The flatten node to visit. * @param value The current value to visit. */ - #visitFlatten(node: Node, value: JSONValue): JSONValue { + #visitFlatten(node: Node, value: JSONObject): JSONObject { const base = this.visit(node.children[0], value); if (!Array.isArray(base)) { return null; @@ -319,7 +318,7 @@ class TreeInterpreter { * @param node The identity node to visit. * @param value The current value to visit. */ - #visitIdentity(_node: Node, value: JSONValue): JSONValue { + #visitIdentity(_node: Node, value: JSONObject): JSONObject { return value; } @@ -329,7 +328,7 @@ class TreeInterpreter { * @param node The index node to visit. * @param value The current value to visit. */ - #visitIndex(node: Node, value: JSONValue): JSONValue { + #visitIndex(node: Node, value: JSONObject): JSONObject { if (!Array.isArray(value)) { return null; } @@ -353,7 +352,7 @@ class TreeInterpreter { * @param node The index expression node to visit. * @param value The current value to visit. */ - #visitIndexExpression(node: Node, value: JSONValue): JSONValue { + #visitIndexExpression(node: Node, value: JSONObject): JSONObject { let result = value; for (const child of node.children) { result = this.visit(child, result); @@ -368,7 +367,7 @@ class TreeInterpreter { * @param node The slice node to visit. * @param value The current value to visit. */ - #visitSlice(node: Node, value: JSONValue): JSONValue { + #visitSlice(node: Node, value: JSONObject): JSONObject { const step = isIntegerNumber(node.children[2]) ? node.children[2] : 1; if (step === 0) { throw new Error('Invalid slice, step cannot be 0'); @@ -394,7 +393,7 @@ class TreeInterpreter { * @param node The key-value pair node to visit. * @param value The current value to visit. */ - #visitKeyValPair(node: Node, value: JSONValue): JSONValue { + #visitKeyValPair(node: Node, value: JSONObject): JSONObject { return this.visit(node.children[0], value); } @@ -404,7 +403,7 @@ class TreeInterpreter { * @param node The literal node to visit. * @param value The current value to visit. */ - #visitLiteral(node: Node, _value: JSONValue): JSONValue { + #visitLiteral(node: Node, _value: JSONObject): JSONObject { return node.value; } @@ -414,11 +413,11 @@ class TreeInterpreter { * @param node The multi-select object node to visit. * @param value The current value to visit. */ - #visitMultiSelectObject(node: Node, value: JSONValue): JSONValue { + #visitMultiSelectObject(node: Node, value: JSONObject): JSONObject { if (Object.is(value, null)) { return null; } - const collected: JSONValue = {}; + const collected: Record = {}; for (const child of node.children) { if (typeof child.value === 'string') { collected[child.value] = this.visit(child, value); @@ -434,7 +433,7 @@ class TreeInterpreter { * @param node The multi-select list node to visit. * @param value The current value to visit. */ - #visitMultiSelectList(node: Node, value: JSONValue): JSONValue { + #visitMultiSelectList(node: Node, value: JSONObject): JSONObject { if (Object.is(value, null)) { return null; } @@ -452,7 +451,7 @@ class TreeInterpreter { * @param node The or expression node to visit. * @param value The current value to visit. */ - #visitOrExpression(node: Node, value: JSONValue): JSONValue { + #visitOrExpression(node: Node, value: JSONObject): JSONObject { const matched = this.visit(node.children[0], value); if (!isTruthy(matched)) { return this.visit(node.children[1], value); @@ -467,7 +466,7 @@ class TreeInterpreter { * @param node The and expression node to visit. * @param value The current value to visit. */ - #visitAndExpression(node: Node, value: JSONValue): JSONValue { + #visitAndExpression(node: Node, value: JSONObject): JSONObject { const matched = this.visit(node.children[0], value); if (!isTruthy(matched)) { return matched; @@ -482,7 +481,7 @@ class TreeInterpreter { * @param node The not expression node to visit. * @param value The current value to visit. */ - #visitNotExpression(node: Node, value: JSONValue): JSONValue { + #visitNotExpression(node: Node, value: JSONObject): JSONObject { const originalResult = this.visit(node.children[0], value); if (typeof originalResult === 'number' && originalResult === 0) { // Special case for 0, !0 should be false, not true. @@ -499,7 +498,7 @@ class TreeInterpreter { * @param node The pipe node to visit. * @param value The current value to visit. */ - #visitPipe(node: Node, value: JSONValue): JSONValue { + #visitPipe(node: Node, value: JSONObject): JSONObject { let result = value; for (const child of node.children) { result = this.visit(child, result); @@ -514,7 +513,7 @@ class TreeInterpreter { * @param node The projection node to visit. * @param value The current value to visit. */ - #visitProjection(node: Node, value: JSONValue): JSONValue { + #visitProjection(node: Node, value: JSONObject): JSONObject { const base = this.visit(node.children[0], value); if (!Array.isArray(base)) { return null; @@ -536,12 +535,12 @@ class TreeInterpreter { * @param node The value projection node to visit. * @param value The current value to visit. */ - #visitValueProjection(node: Node, value: JSONValue): JSONValue { + #visitValueProjection(node: Node, value: JSONObject): JSONObject { const base = this.visit(node.children[0], value); if (!isRecord(base)) { return null; } - const values = Object.values(base); + const values = Object.values(base) as JSONObject[]; const collected = []; for (const item of values) { const current = this.visit(node.children[1], item); diff --git a/packages/jmespath/src/search.ts b/packages/jmespath/src/search.ts index 536a463bf3..8f53775fa5 100644 --- a/packages/jmespath/src/search.ts +++ b/packages/jmespath/src/search.ts @@ -1,6 +1,5 @@ -import type { JSONValue } from '@aws-lambda-powertools/commons/types'; import { Parser } from './Parser.js'; -import type { ParsingOptions } from './types.js'; +import type { ParsingOptions, JSONObject } from './types.js'; const parser = new Parser(); @@ -52,7 +51,7 @@ const parser = new Parser(); */ const search = ( expression: string, - data: JSONValue, + data: JSONObject, options?: ParsingOptions ): unknown => { return parser.parse(expression).search(data, options); diff --git a/packages/jmespath/src/types.ts b/packages/jmespath/src/types.ts index 7ea771e9ba..8fb309a283 100644 --- a/packages/jmespath/src/types.ts +++ b/packages/jmespath/src/types.ts @@ -1,4 +1,7 @@ -import type { JSONValue } from '@aws-lambda-powertools/commons/types'; +import type { + JSONValue, + JSONArray, +} from '@aws-lambda-powertools/commons/types'; import type { Functions } from './Functions.js'; import { BINDING_POWER } from './constants.js'; @@ -91,6 +94,8 @@ type FunctionSignatureOptions = { variadic?: boolean; }; +type JSONObject = JSONArray | JSONValue | object; + export type { FunctionSignatureDecorator, FunctionSignatureOptions, @@ -98,4 +103,5 @@ export type { ParsingOptions, Token, TreeInterpreterOptions, + JSONObject, }; From f4103f3bed6d8a5207babdde1d530c2e888c3501 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 4 Mar 2024 15:42:33 +0100 Subject: [PATCH 075/103] docs(jmespath): documentation & tests --- .../extractDataFromBuiltinEnvelope.json | 20 ++ .../extractDataFromBuiltinEnvelope.ts | 21 +++ .../jmespath/extractDataFromEnvelope.json | 8 + .../jmespath/extractDataFromEnvelope.ts | 31 ++++ docs/snippets/tsconfig.json | 6 +- docs/utilities/jmespath.md | 173 ++++++++++++++++++ packages/commons/package.json | 14 +- packages/commons/src/fromBase64.ts | 15 ++ packages/jmespath/package.json | 8 + packages/jmespath/src/PowertoolsFunctions.ts | 34 ++++ packages/jmespath/src/envelopes.ts | 103 +++++++++++ packages/jmespath/tests/unit/index.test.ts | 10 + 12 files changed, 439 insertions(+), 4 deletions(-) create mode 100644 docs/snippets/jmespath/extractDataFromBuiltinEnvelope.json create mode 100644 docs/snippets/jmespath/extractDataFromBuiltinEnvelope.ts create mode 100644 docs/snippets/jmespath/extractDataFromEnvelope.json create mode 100644 docs/snippets/jmespath/extractDataFromEnvelope.ts create mode 100644 docs/utilities/jmespath.md create mode 100644 packages/commons/src/fromBase64.ts create mode 100644 packages/jmespath/src/PowertoolsFunctions.ts create mode 100644 packages/jmespath/src/envelopes.ts diff --git a/docs/snippets/jmespath/extractDataFromBuiltinEnvelope.json b/docs/snippets/jmespath/extractDataFromBuiltinEnvelope.json new file mode 100644 index 0000000000..9357e9d4b6 --- /dev/null +++ b/docs/snippets/jmespath/extractDataFromBuiltinEnvelope.json @@ -0,0 +1,20 @@ +{ + "Records": [ + { + "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", + "receiptHandle": "MessageReceiptHandle", + "body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\",\"booking\":{\"id\":\"5b2c4803-330b-42b7-811a-c68689425de1\",\"reference\":\"ySz7oA\",\"outboundFlightId\":\"20c0d2f2-56a3-4068-bf20-ff7703db552d\"},\"payment\":{\"receipt\":\"https://pay.stripe.com/receipts/acct_1Dvn7pF4aIiftV70/ch_3JTC14F4aIiftV700iFq2CHB/rcpt_K7QsrFln9FgFnzUuBIiNdkkRYGxUL0X\",\"amount\":100}}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1523232000000", + "SenderId": "123456789012", + "ApproximateFirstReceiveTimestamp": "1523232000001" + }, + "messageAttributes": {}, + "md5OfBody": "7b270e59b47ff90a553787216d55d91d", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue", + "awsRegion": "us-east-1" + } + ] +} diff --git a/docs/snippets/jmespath/extractDataFromBuiltinEnvelope.ts b/docs/snippets/jmespath/extractDataFromBuiltinEnvelope.ts new file mode 100644 index 0000000000..6cd3102f7a --- /dev/null +++ b/docs/snippets/jmespath/extractDataFromBuiltinEnvelope.ts @@ -0,0 +1,21 @@ +import { + extractDataFromEnvelope, + SQS, +} from '@aws-lambda-powertools/jmespath/envelopes'; +import { Logger } from '@aws-lambda-powertools/logger'; +import type { SQSEvent } from 'aws-lambda'; + +const logger = new Logger(); + +type MessageBody = { + customerId: string; +}; + +export const handler = async (event: SQSEvent): Promise => { + const records = extractDataFromEnvelope>(event, SQS); + for (const record of records) { + // records is now a list containing the deserialized body of each message + const { customerId } = record; + logger.appendKeys({ customerId }); + } +}; diff --git a/docs/snippets/jmespath/extractDataFromEnvelope.json b/docs/snippets/jmespath/extractDataFromEnvelope.json new file mode 100644 index 0000000000..a802778bf7 --- /dev/null +++ b/docs/snippets/jmespath/extractDataFromEnvelope.json @@ -0,0 +1,8 @@ +{ + "body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\"}", + "deeplyNested": [ + { + "someData": [1, 2, 3] + } + ] +} diff --git a/docs/snippets/jmespath/extractDataFromEnvelope.ts b/docs/snippets/jmespath/extractDataFromEnvelope.ts new file mode 100644 index 0000000000..2d0f9bccf5 --- /dev/null +++ b/docs/snippets/jmespath/extractDataFromEnvelope.ts @@ -0,0 +1,31 @@ +import { extractDataFromEnvelope } from '@aws-lambda-powertools/jmespath/envelopes'; + +type MyEvent = { + body: string; // "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\"}" + deeplyNested: Array<{ someData: number[] }>; +}; + +type MessageBody = { + customerId: string; +}; + +export const handler = async (event: MyEvent): Promise => { + const payload = extractDataFromEnvelope( + event, + 'powertools_json(body)' + ); + const { customerId } = payload; // now deserialized + + // also works for fetching and flattening deeply nested data + const someData = extractDataFromEnvelope( + event, + 'deeplyNested[*].someData[]' + ); + + return { + customerId, + message: 'success', + context: someData, + statusCode: 200, + }; +}; diff --git a/docs/snippets/tsconfig.json b/docs/snippets/tsconfig.json index 1a3fe8b171..d6aec30ce7 100644 --- a/docs/snippets/tsconfig.json +++ b/docs/snippets/tsconfig.json @@ -27,7 +27,11 @@ "@aws-lambda-powertools/idempotency/middleware": [ "../../packages/idempotency/lib/middleware" ], - "@aws-lambda-powertools/batch": ["../../packages/batch/lib"] + "@aws-lambda-powertools/batch": ["../../packages/batch/lib"], + "@aws-lambda-powertools/jmespath": ["../../packages/jmespath/lib"], + "@aws-lambda-powertools/jmespath/envelopes": [ + "../../packages/jmespath/lib/envelopes" + ] } } } diff --git a/docs/utilities/jmespath.md b/docs/utilities/jmespath.md new file mode 100644 index 0000000000..63933f2e95 --- /dev/null +++ b/docs/utilities/jmespath.md @@ -0,0 +1,173 @@ +--- +title: JMESPath Functions +description: Utility +--- + +???+ tip + JMESPath is a query language for JSON used by tools like the AWS CLI and Powertools for AWS Lambda (TypeScript). + +Built-in [JMESPath](https://jmespath.org/){target="_blank" rel="nofollow"} Functions to easily deserialize common encoded JSON payloads in Lambda functions. + +## Key features + +* Deserialize JSON from JSON strings, base64, and compressed data +* Use JMESPath to extract and combine data recursively +* Provides commonly used JMESPath expression with popular event sources + +## Getting started + +You might have events that contains encoded JSON payloads as string, base64, or even in compressed format. It is a common use case to decode and extract them partially or fully as part of your Lambda function invocation. + +Powertools for AWS Lambda (TypeScript) also have utilities like [idempotency](idempotency.md){target="_blank"} where you might need to extract a portion of your data before using them. + +???+ info "Terminology" + **Envelope** is the terminology we use for the **JMESPath expression** to extract your JSON object from your data input. We might use those two terms interchangeably. + +### Extracting data + +You can use the `extractDataFromEnvelope` function with any [JMESPath expression](https://jmespath.org/tutorial.html){target="_blank" rel="nofollow"}. + +???+ tip + Another common use case is to fetch deeply nested data, filter, flatten, and more. + +=== "extractDataFromBuiltinEnvelope.ts" + ```typescript hl_lines="1 13 20" + --8<-- "docs/snippets/jmespath/extractDataFromEnvelope.ts" + ``` + +=== "extractDataFromEnvelope.json" + + ```json + --8<-- "docs/snippets/jmespath/extractDataFromEnvelope.json" + ``` + +### Built-in envelopes + +We provide built-in envelopes for popular AWS Lambda event sources to easily decode and/or deserialize JSON objects. + +=== "extractDataFromBuiltinEnvelope.ts" + ```typescript hl_lines="2-3 15" + --8<-- "docs/snippets/jmespath/extractDataFromBuiltinEnvelope.ts" + ``` + +=== "extractDataFromBuiltinEnvelope.json" + + ```json hl_lines="6 15" + --8<-- "docs/snippets/jmespath/extractDataFromBuiltinEnvelope.json" + ``` + +These are all built-in envelopes you can use along with their expression as a reference: + +| Envelope | JMESPath expression | +| --------------------------------- | ----------------------------------------------------------------------------------------- | +| **`API_GATEWAY_HTTP`** | `powertools_json(body)` | +| **`API_GATEWAY_REST`** | `powertools_json(body)` | +| **`CLOUDWATCH_EVENTS_SCHEDULED`** | `detail` | +| **`CLOUDWATCH_LOGS`** | `awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]` | +| **`EVENTBRIDGE`** | `detail` | +| **`KINESIS_DATA_STREAM`** | `Records[*].kinesis.powertools_json(powertools_base64(data))` | +| **`S3_EVENTBRIDGE_SQS`** | `Records[*].powertools_json(body).detail` | +| **`S3_KINESIS_FIREHOSE`** | `records[*].powertools_json(powertools_base64(data)).Records[0]` | +| **`S3_SNS_KINESIS_FIREHOSE`** | `records[*].powertools_json(powertools_base64(data)).powertools_json(Message).Records[0]` | +| **`S3_SNS_SQS`** | `Records[*].powertools_json(body).powertools_json(Message).Records[0]` | +| **`S3_SQS`** | `Records[*].powertools_json(body).Records[0]` | +| **`SNS`** | `Records[0].Sns.Message | powertools_json(@)` | +| **`SQS`** | `Records[*].powertools_json(body)` | + +???+ tip "Using SNS?" + If you don't require SNS metadata, enable [raw message delivery](https://docs.aws.amazon.com/sns/latest/dg/sns-large-payload-raw-message-delivery.html). It will reduce multiple payload layers and size, when using SNS in combination with other services (_e.g., SQS, S3, etc_). + +## Advanced + +### Built-in JMESPath functions + +You can use our built-in JMESPath functions within your envelope expression. They handle deserialization for common data formats found in AWS Lambda event sources such as JSON strings, base64, and uncompress gzip data. + +#### powertools_json function + +Use `powertools_json` function to decode any JSON string anywhere a JMESPath expression is allowed. + +> **Idempotency scenario** + +This sample will deserialize the JSON string within the `body` key before [Idempotency](./idempotency.md){target="_blank"} processes it. + +=== "powertools_json_idempotency_jmespath.py" + + ```python hl_lines="16" + --8<-- "examples/jmespath_functions/src/powertools_json_idempotency_jmespath.py" + ``` + +=== "powertools_json_idempotency_jmespath.json" + + ```json hl_lines="28" + --8<-- "examples/jmespath_functions/src/powertools_json_idempotency_jmespath.json" + ``` + +#### powertools_base64 function + +Use `powertools_base64` function to decode any base64 data. + +This sample will decode the base64 value within the `data` key, and deserialize the JSON string before processing. + +=== "powertools_base64_jmespath_function.py" + + ```python hl_lines="7 10 37 49 53 55 57" + --8<-- "examples/jmespath_functions/src/powertools_base64_jmespath_function.py" + ``` + +=== "powertools_base64_jmespath_schema.py" + + ```python hl_lines="7 8 10 12 17 19 24 26 31 33 38 40" + --8<-- "examples/jmespath_functions/src/powertools_base64_jmespath_schema.py" + ``` + +=== "powertools_base64_jmespath_payload.json" + + ```json + --8<-- "examples/jmespath_functions/src/powertools_base64_jmespath_payload.json" + ``` + +#### powertools_base64_gzip function + +Use `powertools_base64_gzip` function to decompress and decode base64 data. + +This sample will decompress and decode base64 data from Cloudwatch Logs, then use JMESPath pipeline expression to pass the result for decoding its JSON string. + +=== "powertools_base64_gzip_jmespath_function.py" + + ```python hl_lines="6 10 15 29 31 33 35" + --8<-- "examples/jmespath_functions/src/powertools_base64_gzip_jmespath_function.py" + ``` + +=== "powertools_base64_gzip_jmespath_schema.py" + + ```python hl_lines="7-15 17 19 24 26 31 33 38 40" + --8<-- "examples/jmespath_functions/src/powertools_base64_gzip_jmespath_schema.py" + ``` + +=== "powertools_base64_gzip_jmespath_payload.json" + + ```json + --8<-- "examples/jmespath_functions/src/powertools_base64_gzip_jmespath_payload.json" + ``` + +### Bring your own JMESPath function + +???+ warning + This should only be used for advanced use cases where you have special formats not covered by the built-in functions. + +For special binary formats that you want to decode before processing, you can bring your own [JMESPath function](https://github.com/jmespath/jmespath.py#custom-functions){target="_blank" rel="nofollow"} and any additional option via `jmespath_options` param. To keep Powertools for AWS Lambda (TypeScript) built-in functions, you can extend the `PowertoolsFunctions` class. + +Here is an example of how to decompress messages using [zlib](https://docs.python.org/3/library/zlib.html){target="_blank" rel="nofollow"}: + +=== "powertools_custom_jmespath_function.py" + + ```python hl_lines="9 14 17-18 23 34 39 41 43" + --8<-- "examples/jmespath_functions/src/powertools_custom_jmespath_function.py" + ``` + +=== "powertools_custom_jmespath_function.json" + + ```json + --8<-- "examples/jmespath_functions/src/powertools_custom_jmespath_function.json" + ``` \ No newline at end of file diff --git a/packages/commons/package.json b/packages/commons/package.json index 3256503909..b2a5bba1f5 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -40,6 +40,10 @@ "default": "./lib/esm/index.js" } }, + "./utils/base64": { + "import": "./lib/esm/fromBase64.js", + "require": "./lib/cjs/fromBase64.js" + }, "./typeutils": { "import": "./lib/esm/typeUtils.js", "require": "./lib/cjs/typeUtils.js" @@ -51,13 +55,17 @@ }, "typesVersions": { "*": { - "types": [ - "lib/cjs/types/index.d.ts", - "lib/esm/types/index.d.ts" + "utils/base64": [ + "lib/cjs/fromBase64.d.ts", + "lib/esm/fromBase64.d.ts" ], "typeutils": [ "lib/cjs/typeUtils.d.ts", "lib/esm/typeUtils.d.ts" + ], + "types": [ + "lib/cjs/types/index.d.ts", + "lib/esm/types/index.d.ts" ] } }, diff --git a/packages/commons/src/fromBase64.ts b/packages/commons/src/fromBase64.ts new file mode 100644 index 0000000000..b3af876113 --- /dev/null +++ b/packages/commons/src/fromBase64.ts @@ -0,0 +1,15 @@ +const BASE64_REGEX = /^[A-Za-z0-9+/]*={0,2}$/; + +const fromBase64 = (input: string, encoding?: BufferEncoding): Uint8Array => { + if ((input.length * 3) % 4 !== 0) { + throw new TypeError(`Incorrect padding on base64 string.`); + } + if (!BASE64_REGEX.exec(input)) { + throw new TypeError(`Invalid base64 string.`); + } + const buffer = encoding ? Buffer.from(input, encoding) : Buffer.from(input); + + return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); +}; + +export { fromBase64 }; diff --git a/packages/jmespath/package.json b/packages/jmespath/package.json index a43a68e817..c4c9645563 100644 --- a/packages/jmespath/package.json +++ b/packages/jmespath/package.json @@ -39,6 +39,10 @@ "default": "./lib/esm/index.js" } }, + "./envelopes": { + "import": "./lib/esm/envelopes.js", + "require": "./lib/cjs/envelopes.js" + }, "./types": { "import": "./lib/esm/types.js", "require": "./lib/cjs/types.js" @@ -49,6 +53,10 @@ "types": [ "lib/cjs/types.d.ts", "lib/esm/types.d.ts" + ], + "envelopes": [ + "lib/cjs/envelopes.d.ts", + "lib/esm/envelopes.d.ts" ] } }, diff --git a/packages/jmespath/src/PowertoolsFunctions.ts b/packages/jmespath/src/PowertoolsFunctions.ts new file mode 100644 index 0000000000..15d4ab6b3d --- /dev/null +++ b/packages/jmespath/src/PowertoolsFunctions.ts @@ -0,0 +1,34 @@ +import zlib from 'node:zlib'; +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; +import { fromBase64 } from '@aws-lambda-powertools/commons/utils/base64'; +import { Functions } from './Functions.js'; + +const decoder = new TextDecoder('utf-8'); + +class PowertoolsFunctions extends Functions { + @Functions.signature({ + argumentsSpecs: [['string']], + }) + public funcPowertoolsBase64(value: string): string { + return decoder.decode(fromBase64(value, 'base64')); + } + + @Functions.signature({ + argumentsSpecs: [['string']], + }) + public funcPowertoolsBase64Gzip(value: string): string { + const encoded = fromBase64(value, 'base64'); + const uncompressed = zlib.gunzipSync(encoded); + + return uncompressed.toString(); + } + + @Functions.signature({ + argumentsSpecs: [['string']], + }) + public funcPowertoolsJson(value: string): JSONValue { + return JSON.parse(value); + } +} + +export { PowertoolsFunctions }; diff --git a/packages/jmespath/src/envelopes.ts b/packages/jmespath/src/envelopes.ts new file mode 100644 index 0000000000..84cf06c383 --- /dev/null +++ b/packages/jmespath/src/envelopes.ts @@ -0,0 +1,103 @@ +import { search } from './search.js'; +import { PowertoolsFunctions } from './PowertoolsFunctions.js'; +import type { ParsingOptions, JSONObject } from './types.js'; + +/** + * Searches and extracts data using JMESPath + * + * Envelope being the JMESPath expression to extract the data you're after + * + * Built-in JMESPath functions include: `powertools_json`, `powertools_base64`, `powertools_base64_gzip` + * + * @example + * ```typescript + * import { extractDataFromEnvelope } from '@aws-lambda-powertools/jmespath/envelopes'; + * + * type CustomEvent = { + * body: string; // "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\"}" + * }; + * + * type EventBody = { + * customerId: string; + * }; + * + * export const handler = async (event: CustomEvent): Promise => { + * const payload = extractDataFromEnvelope(event, "powertools_json(body)"); + * const { customerId } = payload; // now deserialized + * // ... + * }; + * ``` + * + * We provide built-in envelopes for popular AWS Lambda event sources to easily decode and/or deserialize JSON objects. + * + * @example + * ```typescript + * import { + * extractDataFromEnvelope, + * SQS, + * } from '@aws-lambda-powertools/jmespath/envelopes'; + * import type { SQSEvent } from 'aws-lambda'; + * + * type MessageBody = { + * customerId: string; + * }; + * + * export const handler = async (event: SQSEvent): Promise => { + * const records = extractDataFromEnvelope>(event, SQS); + * for (const record in records) { // records is now a list containing the deserialized body of each message + * const { customerId } = record; + * } + * }; + * ``` + * + * @param data The JSON object to search + * @param envelope The JMESPath expression to use + * @param options The parsing options to use + */ +const extractDataFromEnvelope = ( + data: JSONObject, + envelope: string, + options?: ParsingOptions +): T => { + if (!options) { + options = { customFunctions: new PowertoolsFunctions() }; + } + + return search(envelope, data, options) as T; +}; + +const API_GATEWAY_REST = 'powertools_json(body)'; +const API_GATEWAY_HTTP = 'powertools_json(body)'; +const SQS = 'Records[*].powertools_json(body)'; +const SNS = 'Records[0].Sns.Message | powertools_json(@)'; +const EVENTBRIDGE = 'detail'; +const CLOUDWATCH_EVENTS_SCHEDULED = 'detail'; +const KINESIS_DATA_STREAM = + 'Records[*].kinesis.powertools_json(powertools_base64(data))'; +const CLOUDWATCH_LOGS = + 'awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]'; +const S3_SNS_SQS = + 'Records[*].powertools_json(body).powertools_json(Message).Records[0]'; +const S3_SQS = 'Records[*].powertools_json(body).Records[0]'; +const S3_SNS_KINESIS_FIREHOSE = + 'records[*].powertools_json(powertools_base64(data)).powertools_json(Message).Records[0]'; +const S3_KINESIS_FIREHOSE = + 'records[*].powertools_json(powertools_base64(data)).Records[0]'; +const S3_EVENTBRIDGE_SQS = 'Records[*].powertools_json(body).detail'; + +export { + extractDataFromEnvelope, + API_GATEWAY_REST, + API_GATEWAY_HTTP, + SQS, + SNS, + EVENTBRIDGE, + CLOUDWATCH_EVENTS_SCHEDULED, + KINESIS_DATA_STREAM, + CLOUDWATCH_LOGS, + S3_SNS_SQS, + S3_SQS, + S3_SNS_KINESIS_FIREHOSE, + S3_KINESIS_FIREHOSE, + S3_EVENTBRIDGE_SQS, +}; diff --git a/packages/jmespath/tests/unit/index.test.ts b/packages/jmespath/tests/unit/index.test.ts index 896b23459f..0569fe73ce 100644 --- a/packages/jmespath/tests/unit/index.test.ts +++ b/packages/jmespath/tests/unit/index.test.ts @@ -3,6 +3,7 @@ * * @group unit/jmespath/coverage */ +import { JSONValue } from '@aws-lambda-powertools/commons/types'; import { search, EmptyExpressionError, @@ -16,6 +17,15 @@ import { Parser } from '../../src/Parser.js'; import { TreeInterpreter } from '../../src/TreeInterpreter.js'; describe('Coverage tests', () => { + it('does stuff', () => { + class Test {} + const test = new Test(); + + const result = search('foo', test as unknown as JSONValue); + + expect(result).toBe(test); + }); + // These expressions tests are not part of the compliance suite, but are added to ensure coverage describe('expressions', () => { it('throws an error if the expression is not a string', () => { From 1a7f567a708abb8a5f013b9d9c965007f07cc1ff Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 6 Mar 2024 16:05:38 +0100 Subject: [PATCH 076/103] chore: undo rebase changes --- packages/commons/package.json | 2 +- packages/idempotency/package.json | 2 +- packages/jmespath/package.json | 2 +- packages/logger/package.json | 2 +- packages/metrics/package.json | 2 +- packages/parameters/package.json | 2 +- packages/testing/package.json | 2 +- packages/tracer/package.json | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/commons/package.json b/packages/commons/package.json index b2a5bba1f5..ca71a5fc3a 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -91,4 +91,4 @@ "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing" } -} \ No newline at end of file +} diff --git a/packages/idempotency/package.json b/packages/idempotency/package.json index 54664020f0..5984549a7d 100644 --- a/packages/idempotency/package.json +++ b/packages/idempotency/package.json @@ -136,4 +136,4 @@ "aws-sdk-client-mock": "^3.0.1", "aws-sdk-client-mock-jest": "^3.0.1" } -} \ No newline at end of file +} diff --git a/packages/jmespath/package.json b/packages/jmespath/package.json index c4c9645563..930a9f9453 100644 --- a/packages/jmespath/package.json +++ b/packages/jmespath/package.json @@ -84,4 +84,4 @@ "typescript", "nodejs" ] -} \ No newline at end of file +} diff --git a/packages/logger/package.json b/packages/logger/package.json index 136f6760ed..7da74efebb 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -100,4 +100,4 @@ "serverless", "nodejs" ] -} \ No newline at end of file +} diff --git a/packages/metrics/package.json b/packages/metrics/package.json index 31c511cc4e..0c71584273 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -100,4 +100,4 @@ "serverless", "nodejs" ] -} \ No newline at end of file +} diff --git a/packages/parameters/package.json b/packages/parameters/package.json index 2469a717ee..ba05a0d60b 100644 --- a/packages/parameters/package.json +++ b/packages/parameters/package.json @@ -198,4 +198,4 @@ "optional": true } } -} \ No newline at end of file +} diff --git a/packages/testing/package.json b/packages/testing/package.json index 66aea8a0a3..0e470554b9 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -100,4 +100,4 @@ "aws-cdk-lib": "^2.130.0", "esbuild": "^0.20.1" } -} \ No newline at end of file +} diff --git a/packages/tracer/package.json b/packages/tracer/package.json index 1832af85c7..a88df7c2f6 100644 --- a/packages/tracer/package.json +++ b/packages/tracer/package.json @@ -105,4 +105,4 @@ "serverless", "nodejs" ] -} \ No newline at end of file +} From 2dcc515a6bc76c158b77fcaadb3172b9c1a4b8ec Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 6 Mar 2024 16:08:54 +0100 Subject: [PATCH 077/103] chore: undo rebase changes --- .github/scripts/release_patch_package_json.js | 4 +--- examples/cdk/package.json | 2 +- examples/sam/package.json | 2 +- layers/package.json | 2 +- lerna.json | 3 ++- packages/batch/package.json | 2 +- packages/metrics/package.json | 1 - 7 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/scripts/release_patch_package_json.js b/.github/scripts/release_patch_package_json.js index 0f78d81bf3..761c0680a1 100644 --- a/.github/scripts/release_patch_package_json.js +++ b/.github/scripts/release_patch_package_json.js @@ -18,9 +18,7 @@ if (process.argv.length < 3) { const basePath = resolve(process.argv[2]); const packageJsonPath = join(basePath, 'package.json'); const alphaPackages = []; -const betaPackages = [ - '@aws-lambda-powertools/jmespath', -]; +const betaPackages = []; (() => { try { diff --git a/examples/cdk/package.json b/examples/cdk/package.json index cbdb13d0c5..fd29e2fe09 100644 --- a/examples/cdk/package.json +++ b/examples/cdk/package.json @@ -50,4 +50,4 @@ "phin": "^3.7.0", "source-map-support": "^0.5.21" } -} \ No newline at end of file +} diff --git a/examples/sam/package.json b/examples/sam/package.json index 499fc879c6..647596ed14 100644 --- a/examples/sam/package.json +++ b/examples/sam/package.json @@ -41,4 +41,4 @@ "@middy/core": "^4.7.0", "phin": "^3.7.0" } -} \ No newline at end of file +} diff --git a/layers/package.json b/layers/package.json index 6f56ec817f..b2365d1ae8 100644 --- a/layers/package.json +++ b/layers/package.json @@ -43,4 +43,4 @@ "aws-cdk-lib": "^2.130.0", "esbuild": "^0.20.1" } -} \ No newline at end of file +} diff --git a/lerna.json b/lerna.json index 9beb95c0f1..35075ebc65 100644 --- a/lerna.json +++ b/lerna.json @@ -8,6 +8,7 @@ "packages/idempotency", "packages/batch", "packages/testing", + "packages/jmespath", "examples/cdk", "examples/sam", "layers", @@ -16,4 +17,4 @@ "version": "2.0.2", "npmClient": "npm", "message": "chore(release): %s [skip ci]" -} \ No newline at end of file +} diff --git a/packages/batch/package.json b/packages/batch/package.json index de694a5589..3495a7cf08 100644 --- a/packages/batch/package.json +++ b/packages/batch/package.json @@ -79,4 +79,4 @@ "devDependencies": { "@aws-lambda-powertools/testing-utils": "file:../testing" } -} \ No newline at end of file +} diff --git a/packages/metrics/package.json b/packages/metrics/package.json index 0c71584273..477dd9c2cb 100644 --- a/packages/metrics/package.json +++ b/packages/metrics/package.json @@ -12,7 +12,6 @@ "scripts": { "test": "npm run test:unit", "test:unit": "jest --group=unit --detectOpenHandles --coverage --verbose", - "jest": "jest --detectOpenHandles --verbose", "test:e2e:nodejs16x": "RUNTIME=nodejs16x jest --group=e2e", "test:e2e:nodejs18x": "RUNTIME=nodejs18x jest --group=e2e", "test:e2e:nodejs20x": "RUNTIME=nodejs20x jest --group=e2e", From 26296066b3f9af765169360af67cfaa564f18cb2 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 6 Mar 2024 16:11:51 +0100 Subject: [PATCH 078/103] chore: version --- package-lock.json | 4 ++-- packages/jmespath/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index d98212b4bb..c5c91955fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17209,10 +17209,10 @@ }, "packages/jmespath": { "name": "@aws-lambda-powertools/jmespath", - "version": "1.18.1", + "version": "2.0.2", "license": "MIT-0", "dependencies": { - "@aws-lambda-powertools/commons": "^1.18.1" + "@aws-lambda-powertools/commons": "^2.0.2" } }, "packages/logger": { diff --git a/packages/jmespath/package.json b/packages/jmespath/package.json index 930a9f9453..c8ff1bd284 100644 --- a/packages/jmespath/package.json +++ b/packages/jmespath/package.json @@ -1,6 +1,6 @@ { "name": "@aws-lambda-powertools/jmespath", - "version": "1.18.1", + "version": "2.0.2", "description": "A type safe and modern jmespath module to parse and extract data from JSON documents using JMESPath", "author": { "name": "Amazon Web Services", @@ -66,7 +66,7 @@ "lib" ], "dependencies": { - "@aws-lambda-powertools/commons": "^1.18.1" + "@aws-lambda-powertools/commons": "^2.0.2" }, "repository": { "type": "git", From 8d008c4a30a7816145338fe8072585334ac4e19d Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 6 Mar 2024 16:12:35 +0100 Subject: [PATCH 079/103] chore: undo rebase changes --- packages/logger/src/Logger.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/logger/src/Logger.ts b/packages/logger/src/Logger.ts index d454d50a09..3a8859e447 100644 --- a/packages/logger/src/Logger.ts +++ b/packages/logger/src/Logger.ts @@ -640,8 +640,8 @@ class Logger extends Utility implements LoggerInterface { item instanceof Error ? { error: item } : typeof item === 'string' - ? { extra: item } - : item; + ? { extra: item } + : item; additionalLogAttributes = merge(additionalLogAttributes, attributes); }); From d7aebfb6764a6ebe39f32eee432c126846b60cdc Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 6 Mar 2024 17:22:18 +0100 Subject: [PATCH 080/103] test(commons): unit tests for new commons utils --- .../commons/tests/unit/fromBase64.test.ts | 66 ++++ packages/commons/tests/unit/guards.test.ts | 120 ------- packages/commons/tests/unit/typeUtils.test.ts | 329 ++++++++++++++++++ 3 files changed, 395 insertions(+), 120 deletions(-) create mode 100644 packages/commons/tests/unit/fromBase64.test.ts delete mode 100644 packages/commons/tests/unit/guards.test.ts create mode 100644 packages/commons/tests/unit/typeUtils.test.ts diff --git a/packages/commons/tests/unit/fromBase64.test.ts b/packages/commons/tests/unit/fromBase64.test.ts new file mode 100644 index 0000000000..23ee84ecd3 --- /dev/null +++ b/packages/commons/tests/unit/fromBase64.test.ts @@ -0,0 +1,66 @@ +/** + * Test fromBase64 function + * + * @group unit/commons/fromBase64 + */ +import { fromBase64 } from '../../src/fromBase64.js'; + +describe('Function: fromBase64', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + it('returns the Uint8Array from a base64 string', () => { + // Prepare + const base64 = 'aGVsbG8gd29ybGQ='; + const expected = new Uint8Array([ + 97, 71, 86, 115, 98, 71, 56, 103, 100, 50, 57, 121, 98, 71, 81, 61, + ]); + + // Act + const result = fromBase64(base64); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it('throws a TypeError when the base64 string has incorrect padding', () => { + // Prepare + const base64 = 'aGVsbG8gd29ybGQ'; + + // Act + const result = (): Uint8Array => fromBase64(base64); + + // Assess + expect(result).toThrow(TypeError); + expect(result).toThrow(`Incorrect padding on base64 string.`); + }); + + it('throws a TypeError when the base64 string is invalid', () => { + // Prepare + const base64 = 'a-VsbG8gd29ybGQ='; + + // Act + const result = (): Uint8Array => fromBase64(base64); + + // Assess + expect(result).toThrow(TypeError); + expect(result).toThrow(`Invalid base64 string.`); + }); + + it('uses the provided encoding to create the Uint8Array', () => { + // Prepare + const base64 = 'aGVsbG8gd29ybGQ='; + const encoding = 'utf8'; + const expected = new Uint8Array([ + 97, 71, 86, 115, 98, 71, 56, 103, 100, 50, 57, 121, 98, 71, 81, 61, + ]); + + // Act + const result = fromBase64(base64, encoding); + + // Assess + expect(result).toStrictEqual(expected); + }); +}); diff --git a/packages/commons/tests/unit/guards.test.ts b/packages/commons/tests/unit/guards.test.ts deleted file mode 100644 index 53e248e643..0000000000 --- a/packages/commons/tests/unit/guards.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Test guards functions - * - * @group unit/commons/guards - */ -import { - isRecord, - isTruthy, - isNullOrUndefined, - isString, -} from '../../src/index.js'; - -describe('Functions: guards', () => { - beforeEach(() => { - jest.clearAllMocks(); - jest.resetModules(); - }); - - describe('Function: isRecord', () => { - it('returns true when the passed object is a Record', () => { - // Prepare - const obj = { a: 1, b: 2, c: 3 }; - - // Act - const result = isRecord(obj); - - // Assert - expect(result).toBe(true); - }); - - it('returns false when the passed object is not a Record', () => { - // Prepare - const obj = [1, 2, 3]; - - // Act - const result = isRecord(obj); - - // Assert - expect(result).toBe(false); - }); - }); - - describe('Function: isTruthy', () => { - it.each(['hello', 1, true, [1], { foo: 1 }])( - 'returns true when the passed value is truthy', - (testValue) => { - // Prepare - const value = testValue; - - // Act - const result = isTruthy(value); - - // Assert - expect(result).toBe(true); - } - ); - - it.each(['', 0, false, [], {}, Symbol])( - 'returns true when the passed value is falsy', - (testValue) => { - // Prepare - const value = testValue; - - // Act - const result = isTruthy(value); - - // Assert - expect(result).toBe(false); - } - ); - }); - - describe('Function: isNullOrUndefined', () => { - it('returns true when the passed value is null or undefined', () => { - // Prepare - const value = undefined; - - // Act - const result = isNullOrUndefined(value); - - // Assert - expect(result).toBe(true); - }); - - it('returns false when the passed value is not null or undefined', () => { - // Prepare - const value = 'hello'; - - // Act - const result = isNullOrUndefined(value); - - // Assert - expect(result).toBe(false); - }); - }); - - describe('Function: isString', () => { - it('returns true when the passed value is a string', () => { - // Prepare - const value = 'hello'; - - // Act - const result = isString(value); - - // Assert - expect(result).toBe(true); - }); - - it('returns false when the passed value is not a string', () => { - // Prepare - const value = 123; - - // Act - const result = isString(value); - - // Assert - expect(result).toBe(false); - }); - }); -}); diff --git a/packages/commons/tests/unit/typeUtils.test.ts b/packages/commons/tests/unit/typeUtils.test.ts new file mode 100644 index 0000000000..72cd62adc8 --- /dev/null +++ b/packages/commons/tests/unit/typeUtils.test.ts @@ -0,0 +1,329 @@ +/** + * Test type utils functions + * + * @group unit/commons/typeUtils + */ +import { + isRecord, + isTruthy, + isNullOrUndefined, + isString, + isNumber, + isIntegerNumber, + isNull, + getType, + isStrictEqual, +} from '../../src/index.js'; + +describe('Functions: typeUtils', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + describe('Function: isRecord', () => { + it('returns true when the passed object is a Record', () => { + // Prepare + const obj = { a: 1, b: 2, c: 3 }; + + // Act + const result = isRecord(obj); + + // Assess + expect(result).toBe(true); + }); + + it('returns false when the passed object is not a Record', () => { + // Prepare + const obj = [1, 2, 3]; + + // Act + const result = isRecord(obj); + + // Assess + expect(result).toBe(false); + }); + }); + + describe('Function: isTruthy', () => { + it.each(['hello', 1, true, [1], { foo: 1 }])( + 'returns true when the passed value is truthy', + (testValue) => { + // Prepare + const value = testValue; + + // Act + const result = isTruthy(value); + + // Assess + expect(result).toBe(true); + } + ); + + it.each(['', 0, false, [], {}, Symbol])( + 'returns true when the passed value is falsy', + (testValue) => { + // Prepare + const value = testValue; + + // Act + const result = isTruthy(value); + + // Assess + expect(result).toBe(false); + } + ); + }); + + describe('Function: isNullOrUndefined', () => { + it('returns true when the passed value is null or undefined', () => { + // Prepare + const value = undefined; + + // Act + const result = isNullOrUndefined(value); + + // Assess + expect(result).toBe(true); + }); + + it('returns false when the passed value is not null or undefined', () => { + // Prepare + const value = 'hello'; + + // Act + const result = isNullOrUndefined(value); + + // Assess + expect(result).toBe(false); + }); + }); + + describe('Function: isString', () => { + it('returns true when the passed value is a string', () => { + // Prepare + const value = 'hello'; + + // Act + const result = isString(value); + + // Assess + expect(result).toBe(true); + }); + + it('returns false when the passed value is not a string', () => { + // Prepare + const value = 123; + + // Act + const result = isString(value); + + // Assess + expect(result).toBe(false); + }); + }); + + describe('Function: isNumber', () => { + it('returns true when the passed value is a number', () => { + // Prepare + const value = 123; + + // Act + const result = isNumber(value); + + // Assess + expect(result).toBe(true); + }); + + it('returns false when the passed value is not a number', () => { + // Prepare + const value = 'hello'; + + // Act + const result = isNumber(value); + + // Assess + expect(result).toBe(false); + }); + }); + + describe('Function: isIntegerNumber', () => { + it('returns true when the passed value is an integer number', () => { + // Prepare + const value = 123; + + // Act + const result = isIntegerNumber(value); + + // Assess + expect(result).toBe(true); + }); + + it('returns false when the passed value is not an integer number', () => { + // Prepare + const value = 123.45; + + // Act + const result = isIntegerNumber(value); + + // Assess + expect(result).toBe(false); + }); + }); + + describe('Function: isNull', () => { + it('returns true when the passed value is null', () => { + // Prepare + const value = null; + + // Act + const result = isNull(value); + + // Assess + expect(result).toBe(true); + }); + + it('returns false when the passed value is not null', () => { + // Prepare + const value = 'hello'; + + // Act + const result = isNull(value); + + // Assess + expect(result).toBe(false); + }); + }); + + describe('Function: getType', () => { + it.each([ + { + value: [], + expected: 'array', + }, + { + value: {}, + expected: 'object', + }, + { + value: 'hello', + expected: 'string', + }, + { + value: 123, + expected: 'number', + }, + { + value: true, + expected: 'boolean', + }, + { + value: null, + expected: 'null', + }, + { + value: undefined, + expected: 'unknown', + }, + ])( + 'returns the correct type when passed type $expected', + ({ value, expected }) => { + // Act + const result = getType(value); + + // Assess + expect(result).toBe(expected); + } + ); + }); + + describe('Function: isStrictEqual', () => { + it('returns true when the passed values are strictly equal', () => { + // Prepare + const value1 = 123; + const value2 = 123; + + // Act + const result = isStrictEqual(value1, value2); + + // Assess + expect(result).toBe(true); + }); + + it('returns true when the passed arrays are strictly equal', () => { + // Prepare + const value1 = [1, 2, 3]; + const value2 = [1, 2, 3]; + + // Act + const result = isStrictEqual(value1, value2); + + // Assess + expect(result).toBe(true); + }); + + it('returns true when the passed objects are strictly equal', () => { + // Prepare + const value1 = { a: 1, b: 2, c: 3 }; + const value2 = { a: 1, b: 2, c: 3 }; + + // Act + const result = isStrictEqual(value1, value2); + + // Assess + expect(result).toBe(true); + }); + + it('returns false when the passed values are not strictly equal', () => { + // Prepare + const value1 = 123; + const value2 = '123'; + + // Act + const result = isStrictEqual(value1, value2); + + // Assess + expect(result).toBe(false); + }); + + it.each([ + { + value1: [1, 2, 3], + value2: [1, 3, 2], + }, + { + value1: [1, 2, 3], + value2: [1, 2], + }, + ])( + 'returns false when the passed arrays are not strictly equal', + ({ value1, value2 }) => { + // Act + const result = isStrictEqual(value1, value2); + + // Assess + expect(result).toBe(false); + } + ); + + it.each([ + { + value1: { a: 1, b: 2, c: 3 }, + value2: { a: 1, b: 3, c: 2 }, + }, + { + value1: { a: 1, b: 2, c: 3 }, + value2: { a: 1, b: 2 }, + }, + ])( + 'returns false when the passed objects are not strictly equal', + ({ value1, value2 }) => { + // Act + const result = isStrictEqual(value1, value2); + + // Assess + expect(result).toBe(false); + } + ); + }); +}); From 3b6eedf3b32e4600c5612d0c03b71da56027cf03 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 6 Mar 2024 17:24:17 +0100 Subject: [PATCH 081/103] chore: remove unused test --- packages/jmespath/tests/unit/index.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/jmespath/tests/unit/index.test.ts b/packages/jmespath/tests/unit/index.test.ts index 0569fe73ce..896b23459f 100644 --- a/packages/jmespath/tests/unit/index.test.ts +++ b/packages/jmespath/tests/unit/index.test.ts @@ -3,7 +3,6 @@ * * @group unit/jmespath/coverage */ -import { JSONValue } from '@aws-lambda-powertools/commons/types'; import { search, EmptyExpressionError, @@ -17,15 +16,6 @@ import { Parser } from '../../src/Parser.js'; import { TreeInterpreter } from '../../src/TreeInterpreter.js'; describe('Coverage tests', () => { - it('does stuff', () => { - class Test {} - const test = new Test(); - - const result = search('foo', test as unknown as JSONValue); - - expect(result).toBe(test); - }); - // These expressions tests are not part of the compliance suite, but are added to ensure coverage describe('expressions', () => { it('throws an error if the expression is not a string', () => { From 8fdcdfedbf6b7b6f1c998ea5e401fbed7c9352eb Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 7 Mar 2024 18:57:03 +0100 Subject: [PATCH 082/103] docs(jmespath): docs for utility --- .../jmespath/powertoolsBase64GzipJmespath.ts | 13 ++ .../powertoolsBase64GzipJmespathPayload.json | 3 + .../jmespath/powertoolsBase64Jmespath.ts | 13 ++ .../powertoolsBase64JmespathPayload.json | 3 + .../jmespath/powertoolsCustomFunction.json | 9 ++ .../jmespath/powertoolsCustomFunction.ts | 31 +++++ .../powertoolsJsonIdempotencyJmespath.json | 30 +++++ .../powertoolsJsonIdempotencyJmespath.ts | 51 +++++++ docs/utilities/jmespath.md | 65 +++++---- mkdocs.yml | 2 + packages/jmespath/package.json | 18 ++- packages/jmespath/src/Functions.ts | 30 +++++ packages/jmespath/src/PowertoolsFunctions.ts | 9 +- packages/jmespath/src/TreeInterpreter.ts | 14 +- packages/jmespath/tests/unit/index.test.ts | 124 ++++++++++++++++++ 15 files changed, 369 insertions(+), 46 deletions(-) create mode 100644 docs/snippets/jmespath/powertoolsBase64GzipJmespath.ts create mode 100644 docs/snippets/jmespath/powertoolsBase64GzipJmespathPayload.json create mode 100644 docs/snippets/jmespath/powertoolsBase64Jmespath.ts create mode 100644 docs/snippets/jmespath/powertoolsBase64JmespathPayload.json create mode 100644 docs/snippets/jmespath/powertoolsCustomFunction.json create mode 100644 docs/snippets/jmespath/powertoolsCustomFunction.ts create mode 100644 docs/snippets/jmespath/powertoolsJsonIdempotencyJmespath.json create mode 100644 docs/snippets/jmespath/powertoolsJsonIdempotencyJmespath.ts diff --git a/docs/snippets/jmespath/powertoolsBase64GzipJmespath.ts b/docs/snippets/jmespath/powertoolsBase64GzipJmespath.ts new file mode 100644 index 0000000000..5400b9059a --- /dev/null +++ b/docs/snippets/jmespath/powertoolsBase64GzipJmespath.ts @@ -0,0 +1,13 @@ +import { extractDataFromEnvelope } from '@aws-lambda-powertools/jmespath/envelopes'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +export const handler = async (event: { payload: string }): Promise => { + const logGroup = extractDataFromEnvelope( + event, + 'powertools_base64_gzip(payload) | powertools_json(@).logGroup' + ); + + logger.info('Log group name', { logGroup }); +}; diff --git a/docs/snippets/jmespath/powertoolsBase64GzipJmespathPayload.json b/docs/snippets/jmespath/powertoolsBase64GzipJmespathPayload.json new file mode 100644 index 0000000000..470fb13c2e --- /dev/null +++ b/docs/snippets/jmespath/powertoolsBase64GzipJmespathPayload.json @@ -0,0 +1,3 @@ +{ + "payload": "H4sIACZAXl8C/52PzUrEMBhFX2UILpX8tPbHXWHqIOiq3Q1F0ubrWEiakqTWofTdTYYB0YWL2d5zvnuTFellBIOedoiyKH5M0iwnlKH7HZL6dDB6ngLDfLFYctUKjie9gHFaS/sAX1xNEq525QxwFXRGGMEkx4Th491rUZdV3YiIZ6Ljfd+lfSyAtZloacQgAkqSJCGhxM6t7cwwuUGPz4N0YKyvO6I9WDeMPMSo8Z4Ca/kJ6vMEYW5f1MX7W1lVxaG8vqX8hNFdjlc0iCBBSF4ERT/3Pl7RbMGMXF2KZMh/C+gDpNS7RRsp0OaRGzx0/t8e0jgmcczyLCWEePhni/23JWalzjdu0a3ZvgEaNLXeugEAAA==" +} diff --git a/docs/snippets/jmespath/powertoolsBase64Jmespath.ts b/docs/snippets/jmespath/powertoolsBase64Jmespath.ts new file mode 100644 index 0000000000..46e0d7bbf3 --- /dev/null +++ b/docs/snippets/jmespath/powertoolsBase64Jmespath.ts @@ -0,0 +1,13 @@ +import { extractDataFromEnvelope } from '@aws-lambda-powertools/jmespath/envelopes'; +import { Logger } from '@aws-lambda-powertools/logger'; + +const logger = new Logger(); + +export const handler = async (event: { payload: string }): Promise => { + const data = extractDataFromEnvelope( + event, + 'powertools_json(powertools_base64(payload))' + ); + + logger.info('Decoded payload', { data }); +}; diff --git a/docs/snippets/jmespath/powertoolsBase64JmespathPayload.json b/docs/snippets/jmespath/powertoolsBase64JmespathPayload.json new file mode 100644 index 0000000000..eae0118a5c --- /dev/null +++ b/docs/snippets/jmespath/powertoolsBase64JmespathPayload.json @@ -0,0 +1,3 @@ +{ + "payload": "eyJ1c2VyX2lkIjogMTIzLCAicHJvZHVjdF9pZCI6IDEsICJxdWFudGl0eSI6IDIsICJwcmljZSI6IDEwLjQwLCAiY3VycmVuY3kiOiAiVVNEIn0=" +} diff --git a/docs/snippets/jmespath/powertoolsCustomFunction.json b/docs/snippets/jmespath/powertoolsCustomFunction.json new file mode 100644 index 0000000000..0d098b0c78 --- /dev/null +++ b/docs/snippets/jmespath/powertoolsCustomFunction.json @@ -0,0 +1,9 @@ +{ + "Records": [ + { + "application": "app", + "datetime": "2022-01-01T00:00:00.000Z", + "notification": "GyYA+AXhZKk/K5DkanoQSTYpSKMwwxXh8DRWVo9A1hLqAQ==" + } + ] +} diff --git a/docs/snippets/jmespath/powertoolsCustomFunction.ts b/docs/snippets/jmespath/powertoolsCustomFunction.ts new file mode 100644 index 0000000000..6328cb3ca1 --- /dev/null +++ b/docs/snippets/jmespath/powertoolsCustomFunction.ts @@ -0,0 +1,31 @@ +import { fromBase64 } from '@aws-lambda-powertools/commons/utils/base64'; +import { extractDataFromEnvelope } from '@aws-lambda-powertools/jmespath/envelopes'; +import { PowertoolsFunctions } from '@aws-lambda-powertools/jmespath/functions'; +import { Logger } from '@aws-lambda-powertools/logger'; +import { brotliDecompressSync } from 'node:zlib'; + +const logger = new Logger(); + +// prettier-ignore +class CustomFunctions extends PowertoolsFunctions { + @PowertoolsFunctions.signature({ // (1)! + argumentsSpecs: [['string']], + variadic: false, + }) + public funcDecodeBrotliCompression(value: string): string { // (2)! + const encoded = fromBase64(value, 'base64'); + const uncompressed = brotliDecompressSync(encoded); + + return uncompressed.toString(); + } +} + +export const handler = async (event: { payload: string }): Promise => { + const message = extractDataFromEnvelope( + event, + 'Records[*].decode_brotli_compression(notification) | [*].powertools_json(@).message', + { customFunctions: new CustomFunctions() } + ); + + logger.info('Decoded message', { message }); +}; diff --git a/docs/snippets/jmespath/powertoolsJsonIdempotencyJmespath.json b/docs/snippets/jmespath/powertoolsJsonIdempotencyJmespath.json new file mode 100644 index 0000000000..0534d6bacd --- /dev/null +++ b/docs/snippets/jmespath/powertoolsJsonIdempotencyJmespath.json @@ -0,0 +1,30 @@ +{ + "version": "2.0", + "routeKey": "ANY /createpayment", + "rawPath": "/createpayment", + "rawQueryString": "", + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/createpayment", + "protocol": "HTTP/1.1", + "sourceIp": "ip", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "ANY /createpayment", + "stage": "$default", + "time": "10/Feb/2021:13:40:43 +0000", + "timeEpoch": 1612964443723 + }, + "body": "{\"user\":\"xyz\",\"product_id\":\"123456789\"}", + "isBase64Encoded": false +} diff --git a/docs/snippets/jmespath/powertoolsJsonIdempotencyJmespath.ts b/docs/snippets/jmespath/powertoolsJsonIdempotencyJmespath.ts new file mode 100644 index 0000000000..5ce144a109 --- /dev/null +++ b/docs/snippets/jmespath/powertoolsJsonIdempotencyJmespath.ts @@ -0,0 +1,51 @@ +import { + IdempotencyConfig, + makeIdempotent, +} from '@aws-lambda-powertools/idempotency'; +import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb'; +import type { APIGatewayEvent } from 'aws-lambda'; +import { randomUUID } from 'node:crypto'; + +const persistenceStore = new DynamoDBPersistenceLayer({ + tableName: 'IdempotencyTable', +}); + +export const handler = makeIdempotent( + async (event: APIGatewayEvent) => { + const body = JSON.parse(event.body || '{}'); + const { user, productId } = body; + + const result = await createSubscriptionPayment(user, productId); + + return { + statusCode: 200, + body: JSON.stringify({ + paymentId: result.id, + message: 'success', + }), + }; + }, + { + persistenceStore, + config: new IdempotencyConfig({ + eventKeyJmesPath: 'powertools_json(body)', + }), + } +); + +const createSubscriptionPayment = async ( + user: string, + productId: string +): Promise<{ id: string; message: string }> => { + const payload = { user, productId }; + const response = await fetch('https://httpbin.org/anything', { + method: 'POST', + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error('Failed to create subscription payment'); + } + + return { id: randomUUID(), message: 'paid' }; +}; diff --git a/docs/utilities/jmespath.md b/docs/utilities/jmespath.md index 63933f2e95..4cf9fe3b5f 100644 --- a/docs/utilities/jmespath.md +++ b/docs/utilities/jmespath.md @@ -31,7 +31,7 @@ You can use the `extractDataFromEnvelope` function with any [JMESPath expression Another common use case is to fetch deeply nested data, filter, flatten, and more. === "extractDataFromBuiltinEnvelope.ts" - ```typescript hl_lines="1 13 20" + ```typescript hl_lines="1 13 17 20 22" --8<-- "docs/snippets/jmespath/extractDataFromEnvelope.ts" ``` @@ -91,16 +91,16 @@ Use `powertools_json` function to decode any JSON string anywhere a JMESPath exp This sample will deserialize the JSON string within the `body` key before [Idempotency](./idempotency.md){target="_blank"} processes it. -=== "powertools_json_idempotency_jmespath.py" +=== "powertoolsJsonIdempotencyJmespath.ts" - ```python hl_lines="16" - --8<-- "examples/jmespath_functions/src/powertools_json_idempotency_jmespath.py" + ```ts hl_lines="31" + --8<-- "docs/snippets/jmespath/powertoolsJsonIdempotencyJmespath.ts" ``` -=== "powertools_json_idempotency_jmespath.json" +=== "powertoolsJsonIdempotencyJmespath.json" ```json hl_lines="28" - --8<-- "examples/jmespath_functions/src/powertools_json_idempotency_jmespath.json" + --8<-- "docs/snippets/jmespath/powertoolsJsonIdempotencyJmespath.json" ``` #### powertools_base64 function @@ -109,22 +109,16 @@ Use `powertools_base64` function to decode any base64 data. This sample will decode the base64 value within the `data` key, and deserialize the JSON string before processing. -=== "powertools_base64_jmespath_function.py" +=== "powertoolsBase64Jmespath.ts" - ```python hl_lines="7 10 37 49 53 55 57" - --8<-- "examples/jmespath_functions/src/powertools_base64_jmespath_function.py" + ```ts hl_lines="9" + --8<-- "docs/snippets/jmespath/powertoolsBase64Jmespath.ts" ``` -=== "powertools_base64_jmespath_schema.py" - - ```python hl_lines="7 8 10 12 17 19 24 26 31 33 38 40" - --8<-- "examples/jmespath_functions/src/powertools_base64_jmespath_schema.py" - ``` - -=== "powertools_base64_jmespath_payload.json" +=== "powertoolsBase64JmespathPayload.json" ```json - --8<-- "examples/jmespath_functions/src/powertools_base64_jmespath_payload.json" + --8<-- "docs/snippets/jmespath/powertoolsBase64JmespathPayload.json" ``` #### powertools_base64_gzip function @@ -133,22 +127,16 @@ Use `powertools_base64_gzip` function to decompress and decode base64 data. This sample will decompress and decode base64 data from Cloudwatch Logs, then use JMESPath pipeline expression to pass the result for decoding its JSON string. -=== "powertools_base64_gzip_jmespath_function.py" - - ```python hl_lines="6 10 15 29 31 33 35" - --8<-- "examples/jmespath_functions/src/powertools_base64_gzip_jmespath_function.py" - ``` - -=== "powertools_base64_gzip_jmespath_schema.py" +=== "powertoolsBase64GzipJmespath.ts" - ```python hl_lines="7-15 17 19 24 26 31 33 38 40" - --8<-- "examples/jmespath_functions/src/powertools_base64_gzip_jmespath_schema.py" + ```ts hl_lines="9" + --8<-- "docs/snippets/jmespath/powertoolsBase64GzipJmespath.ts" ``` -=== "powertools_base64_gzip_jmespath_payload.json" +=== "powertoolsBase64GzipJmespathPayload.json" ```json - --8<-- "examples/jmespath_functions/src/powertools_base64_gzip_jmespath_payload.json" + --8<-- "docs/snippets/jmespath/powertoolsBase64GzipJmespathPayload.json" ``` ### Bring your own JMESPath function @@ -156,18 +144,25 @@ This sample will decompress and decode base64 data from Cloudwatch Logs, then us ???+ warning This should only be used for advanced use cases where you have special formats not covered by the built-in functions. -For special binary formats that you want to decode before processing, you can bring your own [JMESPath function](https://github.com/jmespath/jmespath.py#custom-functions){target="_blank" rel="nofollow"} and any additional option via `jmespath_options` param. To keep Powertools for AWS Lambda (TypeScript) built-in functions, you can extend the `PowertoolsFunctions` class. +For special binary formats that you want to decode before processing, you can bring your own JMESPath function by extending the `PowertoolsFunctions` class. -Here is an example of how to decompress messages using [zlib](https://docs.python.org/3/library/zlib.html){target="_blank" rel="nofollow"}: +Here is an example of how to decompress messages compressed using the [Brotli compression algorithm](https://nodejs.org/api/zlib.html#zlibbrotlidecompressbuffer-options-callback){target="_blank" rel="nofollow"}: -=== "powertools_custom_jmespath_function.py" +=== "PowertoolsCustomFunction.ts" - ```python hl_lines="9 14 17-18 23 34 39 41 43" - --8<-- "examples/jmespath_functions/src/powertools_custom_jmespath_function.py" + ```ts hl_lines="3 9 25-26" + --8<-- + docs/snippets/jmespath/powertoolsCustomFunction.ts::8 + docs/snippets/jmespath/powertoolsCustomFunction.ts:10: + + --8<-- ``` -=== "powertools_custom_jmespath_function.json" + 1. The function signature can be enforced at runtime by using the `@Functions.signature` decorator. + 2. The name of the function must start with the `func` prefix. + +=== "powertoolsCustomFunction.json" ```json - --8<-- "examples/jmespath_functions/src/powertools_custom_jmespath_function.json" + --8<-- "docs/snippets/jmespath/powertoolsCustomFunction.json" ``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index e7556ff610..107ca2b45f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,6 +19,7 @@ nav: - utilities/parameters.md - utilities/idempotency.md - utilities/batch.md + - utilities/jmespath.md - Processes: - Roadmap: roadmap.md - Versioning policy: versioning.md @@ -100,6 +101,7 @@ plugins: glob: - snippets/node_modules/* - snippets/package.json + - snippets/CHANGELOG.md extra_css: - stylesheets/extra.css diff --git a/packages/jmespath/package.json b/packages/jmespath/package.json index c8ff1bd284..871d4ac075 100644 --- a/packages/jmespath/package.json +++ b/packages/jmespath/package.json @@ -43,6 +43,10 @@ "import": "./lib/esm/envelopes.js", "require": "./lib/cjs/envelopes.js" }, + "./functions": { + "import": "./lib/esm/PowertoolsFunctions.js", + "require": "./lib/cjs/PowertoolsFunctions.js" + }, "./types": { "import": "./lib/esm/types.js", "require": "./lib/cjs/types.js" @@ -50,13 +54,17 @@ }, "typesVersions": { "*": { - "types": [ - "lib/cjs/types.d.ts", - "lib/esm/types.d.ts" - ], "envelopes": [ "lib/cjs/envelopes.d.ts", "lib/esm/envelopes.d.ts" + ], + "functions": [ + "lib/cjs/PowertoolsFunctions.d.ts", + "lib/esm/PowertoolsFunctions.d.ts" + ], + "types": [ + "lib/cjs/types.d.ts", + "lib/esm/types.d.ts" ] } }, @@ -84,4 +92,4 @@ "typescript", "nodejs" ] -} +} \ No newline at end of file diff --git a/packages/jmespath/src/Functions.ts b/packages/jmespath/src/Functions.ts index c8f6ec72e4..69a9fb7907 100644 --- a/packages/jmespath/src/Functions.ts +++ b/packages/jmespath/src/Functions.ts @@ -48,6 +48,7 @@ import { arityCheck, typeCheck } from './utils.js'; * ``` */ class Functions { + public methods: Set = new Set(); /** * Get the absolute value of the provided number. * @@ -540,6 +541,35 @@ class Functions { return Object.values(arg); } + public introspectMethods(scope?: Functions): Set { + const prototype = Object.getPrototypeOf(this); + const ownName = prototype.constructor.name; + const methods = new Set(); + if (ownName !== 'Functions') { + for (const method of prototype.introspectMethods(scope)) { + methods.add(method); + } + } + + // This block is executed for every class in the inheritance chain + for (const method of Object.getOwnPropertyNames( + Object.getPrototypeOf(this) + )) { + method !== 'constructor' && + method.startsWith('func') && + methods.add(method); + } + + // This block will be executed only if the scope is the outermost class + if (this.methods) { + for (const method of methods) { + this.methods.add(method); + } + } + + return methods; + } + /** * Decorator to enforce the signature of a function at runtime. * diff --git a/packages/jmespath/src/PowertoolsFunctions.ts b/packages/jmespath/src/PowertoolsFunctions.ts index 15d4ab6b3d..3d4c2dbb37 100644 --- a/packages/jmespath/src/PowertoolsFunctions.ts +++ b/packages/jmespath/src/PowertoolsFunctions.ts @@ -1,10 +1,15 @@ -import zlib from 'node:zlib'; +import { gunzipSync } from 'node:zlib'; import type { JSONValue } from '@aws-lambda-powertools/commons/types'; import { fromBase64 } from '@aws-lambda-powertools/commons/utils/base64'; import { Functions } from './Functions.js'; const decoder = new TextDecoder('utf-8'); +/** + * Custom functions for the Powertools for AWS Lambda JMESPath module. + * + * Built-in JMESPath functions include: `powertools_json`, `powertools_base64`, `powertools_base64_gzip` + */ class PowertoolsFunctions extends Functions { @Functions.signature({ argumentsSpecs: [['string']], @@ -18,7 +23,7 @@ class PowertoolsFunctions extends Functions { }) public funcPowertoolsBase64Gzip(value: string): string { const encoded = fromBase64(value, 'base64'); - const uncompressed = zlib.gunzipSync(encoded); + const uncompressed = gunzipSync(encoded); return uncompressed.toString(); } diff --git a/packages/jmespath/src/TreeInterpreter.ts b/packages/jmespath/src/TreeInterpreter.ts index 8030f559f8..2740260230 100644 --- a/packages/jmespath/src/TreeInterpreter.ts +++ b/packages/jmespath/src/TreeInterpreter.ts @@ -217,7 +217,10 @@ class TreeInterpreter { `Function name must be a string, got ${node.value}` ); } - // get all methods of the functions object + if (this.#functions.methods.size === 0) { + this.#functions.introspectMethods(); + } + /* // get all methods of the functions object const functionsProto = Object.getPrototypeOf(this.#functions); const methods = [ ...Object.getOwnPropertyNames(functionsProto), @@ -229,7 +232,7 @@ class TreeInterpreter { Object.getPrototypeOf(this.#functions).__proto__ ) : []), - ]; + ]; */ // convert snake_case to camelCase const normalizedFunctionName = node.value.replace(/_([a-z])/g, (g) => g[1].toUpperCase() @@ -239,9 +242,12 @@ class TreeInterpreter { normalizedFunctionName.charAt(0).toUpperCase() + normalizedFunctionName.slice(1) }`; - const methodName = methods.find((method) => method === funcName); + /* const methodName = methods.find((method) => method === funcName); if (!methodName) { throw new UnknownFunctionError(node.value); + } */ + if (!this.#functions.methods.has(funcName)) { + throw new UnknownFunctionError(node.value); } try { @@ -251,7 +257,7 @@ class TreeInterpreter { // we also want to keep the args generic, so for now we'll just ignore it. // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore-next-line - return this.#functions[methodName](args); + return this.#functions[funcName](args); } catch (error) { if ( error instanceof JMESPathTypeError || diff --git a/packages/jmespath/tests/unit/index.test.ts b/packages/jmespath/tests/unit/index.test.ts index 896b23459f..052fcd7c07 100644 --- a/packages/jmespath/tests/unit/index.test.ts +++ b/packages/jmespath/tests/unit/index.test.ts @@ -3,6 +3,7 @@ * * @group unit/jmespath/coverage */ +import { fromBase64 } from '@aws-lambda-powertools/commons/utils/base64'; import { search, EmptyExpressionError, @@ -14,6 +15,9 @@ import { import { Functions } from '../../src/Functions.js'; import { Parser } from '../../src/Parser.js'; import { TreeInterpreter } from '../../src/TreeInterpreter.js'; +import { brotliDecompressSync } from 'node:zlib'; +import { PowertoolsFunctions } from '../../src/PowertoolsFunctions.js'; +import { extractDataFromEnvelope, SQS } from '../../src/envelopes.js'; describe('Coverage tests', () => { // These expressions tests are not part of the compliance suite, but are added to ensure coverage @@ -253,4 +257,124 @@ describe('Coverage tests', () => { expect(result).toEqual([]); }); }); + + describe('function: extractDataFromEnvelope', () => { + it('extracts the data from a known envelope', () => { + // Prepare + const event = { + Records: [ + { + body: '{"foo":"bar"}', + }, + ], + }; + + // Act + const data = extractDataFromEnvelope(event, SQS); + + // Assess + expect(data).toStrictEqual([{ foo: 'bar' }]); + }); + }); + + describe('class: PowertoolsFunctions', () => { + it('decodes a json string', () => { + // Prepare + const event = '{"user":"xyz","product_id":"123456789"}'; + + // Act + const data = extractDataFromEnvelope(event, 'powertools_json(@)', { + customFunctions: new PowertoolsFunctions(), + }); + + // Assess + expect(data).toStrictEqual({ + user: 'xyz', + product_id: '123456789', + }); + }); + + it('decodes a base64 gzip string', () => { + // Prepare + const event = { + payload: + 'H4sIACZAXl8C/52PzUrEMBhFX2UILpX8tPbHXWHqIOiq3Q1F0ubrWEiakqTWofTdTYYB0YWL2d5zvnuTFellBIOedoiyKH5M0iwnlKH7HZL6dDB6ngLDfLFYctUKjie9gHFaS/sAX1xNEq525QxwFXRGGMEkx4Th491rUZdV3YiIZ6Ljfd+lfSyAtZloacQgAkqSJCGhxM6t7cwwuUGPz4N0YKyvO6I9WDeMPMSo8Z4Ca/kJ6vMEYW5f1MX7W1lVxaG8vqX8hNFdjlc0iCBBSF4ERT/3Pl7RbMGMXF2KZMh/C+gDpNS7RRsp0OaRGzx0/t8e0jgmcczyLCWEePhni/23JWalzjdu0a3ZvgEaNLXeugEAAA==', + }; + + // Act + const data = extractDataFromEnvelope( + event, + 'powertools_base64_gzip(payload) | powertools_json(@).logGroup', + { + customFunctions: new PowertoolsFunctions(), + } + ); + + // Assess + expect(data).toStrictEqual('/aws/lambda/powertools-example'); + }); + + it('decodes a base64 string', () => { + // Prepare + const event = { + payload: + 'eyJ1c2VyX2lkIjogMTIzLCAicHJvZHVjdF9pZCI6IDEsICJxdWFudGl0eSI6IDIsICJwcmljZSI6IDEwLjQwLCAiY3VycmVuY3kiOiAiVVNEIn0=', + }; + + // Act + const data = extractDataFromEnvelope( + event, + 'powertools_json(powertools_base64(payload))', + { + customFunctions: new PowertoolsFunctions(), + } + ); + + // Assess + expect(data).toStrictEqual({ + user_id: 123, + product_id: 1, + quantity: 2, + price: 10.4, + currency: 'USD', + }); + }); + + it('uses the custom function extending the powertools custom functions', () => { + // Prepare + class CustomFunctions extends PowertoolsFunctions { + public constructor() { + super(); + } + @PowertoolsFunctions.signature({ + argumentsSpecs: [['string']], + }) + public funcDecodeBrotliCompression(value: string): string { + const encoded = fromBase64(value, 'base64'); + const uncompressed = brotliDecompressSync(encoded); + + return uncompressed.toString(); + } + } + const event = { + Records: [ + { + application: 'messaging-app', + datetime: '2022-01-01T00:00:00.000Z', + notification: 'GyYA+AXhZKk/K5DkanoQSTYpSKMwwxXh8DRWVo9A1hLqAQ==', + }, + ], + }; + + // Act + const messages = extractDataFromEnvelope( + event, + 'Records[*].decode_brotli_compression(notification) | [*].powertools_json(@).message', + { customFunctions: new CustomFunctions() } + ); + + // Assess + expect(messages).toStrictEqual(['hello world']); + }); + }); }); From 7c9606257000020342757777cfe4af1f7565abc0 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 7 Mar 2024 18:59:22 +0100 Subject: [PATCH 083/103] chore: remove unused lines --- packages/jmespath/src/TreeInterpreter.ts | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/packages/jmespath/src/TreeInterpreter.ts b/packages/jmespath/src/TreeInterpreter.ts index 2740260230..55ab0673fc 100644 --- a/packages/jmespath/src/TreeInterpreter.ts +++ b/packages/jmespath/src/TreeInterpreter.ts @@ -36,6 +36,7 @@ class TreeInterpreter { } else { this.#functions = new Functions(); } + this.#functions.introspectMethods(); } /** @@ -217,22 +218,6 @@ class TreeInterpreter { `Function name must be a string, got ${node.value}` ); } - if (this.#functions.methods.size === 0) { - this.#functions.introspectMethods(); - } - /* // get all methods of the functions object - const functionsProto = Object.getPrototypeOf(this.#functions); - const methods = [ - ...Object.getOwnPropertyNames(functionsProto), - // If the functions object's prototype is the Functions class, then it - // must be a custom functions object, so we'll also include the methods - // from the Functions class itself. - ...(functionsProto.__proto__.constructor.name === 'Functions' - ? Object.getOwnPropertyNames( - Object.getPrototypeOf(this.#functions).__proto__ - ) - : []), - ]; */ // convert snake_case to camelCase const normalizedFunctionName = node.value.replace(/_([a-z])/g, (g) => g[1].toUpperCase() @@ -242,10 +227,6 @@ class TreeInterpreter { normalizedFunctionName.charAt(0).toUpperCase() + normalizedFunctionName.slice(1) }`; - /* const methodName = methods.find((method) => method === funcName); - if (!methodName) { - throw new UnknownFunctionError(node.value); - } */ if (!this.#functions.methods.has(funcName)) { throw new UnknownFunctionError(node.value); } From b5c8a0d0ba6d0488677736ac0aa745135ca56969 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 7 Mar 2024 19:18:02 +0100 Subject: [PATCH 084/103] docs: api docs --- packages/jmespath/src/Functions.ts | 54 ++++++-------------- packages/jmespath/src/PowertoolsFunctions.ts | 22 ++++++++ packages/jmespath/typedoc.json | 9 +++- 3 files changed, 44 insertions(+), 41 deletions(-) diff --git a/packages/jmespath/src/Functions.ts b/packages/jmespath/src/Functions.ts index 69a9fb7907..a511c49974 100644 --- a/packages/jmespath/src/Functions.ts +++ b/packages/jmespath/src/Functions.ts @@ -53,7 +53,6 @@ class Functions { * Get the absolute value of the provided number. * * @param args The number to get the absolute value of - * @returns The absolute value of the number */ @Functions.signature({ argumentsSpecs: [['number']] }) public funcAbs(args: number): number { @@ -64,7 +63,6 @@ class Functions { * Calculate the average of the numbers in the provided array. * * @param args The numbers to average - * @returns The average of the numbers */ @Functions.signature({ argumentsSpecs: [['array-number']], @@ -77,7 +75,6 @@ class Functions { * Get the ceiling of the provided number. * * @param args The number to get the ceiling of - * @returns The ceiling of the number */ @Functions.signature({ argumentsSpecs: [['number']] }) public funcCeil(args: number): number { @@ -89,7 +86,6 @@ class Functions { * * @param haystack The array or string to check * @param needle The value to check for - * @returns True if the value is in the array or string, false otherwise */ @Functions.signature({ argumentsSpecs: [['array', 'string'], ['any']], @@ -101,8 +97,8 @@ class Functions { /** * Determines if the provided string ends with the provided suffix. * - * @param args The string to check - * @returns True if the string ends with the suffix, false otherwise + * @param str The string to check + * @param suffix The suffix to check for */ @Functions.signature({ argumentsSpecs: [['string'], ['string']], @@ -115,7 +111,6 @@ class Functions { * Get the floor of the provided number. * * @param args The number to get the floor of - * @returns The floor of the number */ @Functions.signature({ argumentsSpecs: [['number']] }) public funcFloor(args: number): number { @@ -127,7 +122,6 @@ class Functions { * * @param separator The separator to use * @param items The array of itmes to join - * @returns The joined array */ @Functions.signature({ argumentsSpecs: [['string'], ['array-string']], @@ -139,8 +133,7 @@ class Functions { /** * Get the keys of the provided object. * - * @param args The object to get the keys of - * @returns The keys of the object + * @param arg The object to get the keys of */ @Functions.signature({ argumentsSpecs: [['object']], @@ -152,8 +145,7 @@ class Functions { /** * Get the number of items in the provided item. * - * @param args The array to get the length of - * @returns The length of the array + * @param arg The array to get the length of */ @Functions.signature({ argumentsSpecs: [['array', 'string', 'object']], @@ -173,7 +165,6 @@ class Functions { * * @param expression The expression to map over the array * @param args The array to map the expression over - * @returns The result of mapping the expression over the array */ @Functions.signature({ argumentsSpecs: [['any'], ['array']], @@ -190,8 +181,7 @@ class Functions { /** * Get the maximum value in the provided array. * - * @param args The array to get the maximum value of - * @returns The maximum value in the array + * @param arg The array to get the maximum value of */ @Functions.signature({ argumentsSpecs: [['array-number', 'array-string']], @@ -213,7 +203,6 @@ class Functions { * * @param args The array of items to get the maximum value of * @param expression The expression to evaluate for each item in the array - * @returns The item in the array that has the maximum value when the expression is evaluated */ @Functions.signature({ argumentsSpecs: [['array'], ['expression']], @@ -262,7 +251,6 @@ class Functions { * Note that this is a shallow merge and will not merge nested objects. * * @param args The objects to merge - * @returns The merged object */ @Functions.signature({ argumentsSpecs: [['object']], @@ -275,8 +263,7 @@ class Functions { /** * Get the minimum value in the provided array. * - * @param args The array to get the minimum value of - * @returns The minimum value in the array + * @param arg The array to get the minimum value of */ @Functions.signature({ argumentsSpecs: [['array-number', 'array-string']], @@ -297,7 +284,6 @@ class Functions { * * @param args The array of items to get the minimum value of * @param expression The expression to evaluate for each item in the array - * @returns The item in the array that has the minimum value when the expression is evaluated */ @Functions.signature({ argumentsSpecs: [['array'], ['expression']], @@ -345,7 +331,6 @@ class Functions { * If all arguments evaluate to null, then null is returned. * * @param args The keys of the items to check - * @returns The first key that is not null or null if all keys are null */ @Functions.signature({ argumentsSpecs: [[]], @@ -358,8 +343,7 @@ class Functions { /** * Reverses the provided string or array. * - * @param args The string or array to reverse - * @returns The reversed string or array + * @param arg The string or array to reverse */ @Functions.signature({ argumentsSpecs: [['string', 'array']], @@ -374,7 +358,6 @@ class Functions { * Sort the provided array. * * @param arg The array to sort - * @returns The sorted array */ @Functions.signature({ argumentsSpecs: [['array-number', 'array-string']], @@ -386,9 +369,8 @@ class Functions { /** * Sort the provided array by the provided expression. * - * @param arg The array to sort + * @param args The array to sort * @param expression The expression to sort by - * @returns The sorted array */ @Functions.signature({ argumentsSpecs: [['array'], ['expression']], @@ -430,23 +412,22 @@ class Functions { } /** - * Determines if the provided string starts with the provided suffix. + * Determines if the provided string starts with the provided prefix. * - * @param args The string to check - * @returns True if the string ends with the suffix, false otherwise + * @param str The string to check + * @param prefix The prefix to check for */ @Functions.signature({ argumentsSpecs: [['string'], ['string']], }) - public funcStartsWith(str: string, suffix: string): boolean { - return str.startsWith(suffix); + public funcStartsWith(str: string, prefix: string): boolean { + return str.startsWith(prefix); } /** * Sum the provided numbers. * * @param args The numbers to sum - * @returns The sum of the numbers */ @Functions.signature({ argumentsSpecs: [['array-number']], @@ -461,8 +442,7 @@ class Functions { * If the provided value is an array, then it is returned. * Otherwise, the value is wrapped in an array and returned. * - * @param args The items to convert to an array - * @returns The items as an array + * @param arg The items to convert to an array */ @Functions.signature({ argumentsSpecs: [['any']], @@ -482,7 +462,6 @@ class Functions { * If the value cannot be converted to a number, then null is returned. * * @param arg The value to convert to a number - * @returns The value as a number or null if the value cannot be converted to a number */ @Functions.signature({ argumentsSpecs: [['any']], @@ -506,7 +485,6 @@ class Functions { * Otherwise, the value is converted to a string and returned. * * @param arg The value to convert to a string - * @returns The value as a string */ @Functions.signature({ argumentsSpecs: [['any']], @@ -519,7 +497,6 @@ class Functions { * Get the type of the provided value. * * @param arg The value to check the type of - * @returns The type of the value */ @Functions.signature({ argumentsSpecs: [['any']], @@ -531,8 +508,7 @@ class Functions { /** * Get the values of the provided object. * - * @param args The object to get the values of - * @returns The values of the object + * @param arg The object to get the values of */ @Functions.signature({ argumentsSpecs: [['object']], diff --git a/packages/jmespath/src/PowertoolsFunctions.ts b/packages/jmespath/src/PowertoolsFunctions.ts index 3d4c2dbb37..3cba848f73 100644 --- a/packages/jmespath/src/PowertoolsFunctions.ts +++ b/packages/jmespath/src/PowertoolsFunctions.ts @@ -9,6 +9,28 @@ const decoder = new TextDecoder('utf-8'); * Custom functions for the Powertools for AWS Lambda JMESPath module. * * Built-in JMESPath functions include: `powertools_json`, `powertools_base64`, `powertools_base64_gzip` + * + * You can use these functions to decode and/or deserialize JSON objects when using the {@link index.search | search} function. + * + * @example + * ```typescript + * import { search } from '@aws-lambda-powertools/jmespath'; + * import { PowertoolsFunctions } from '@aws-lambda-powertools/jmespath/functions'; + * + * const data = { + * body: "{\"foo\": \"bar\"}" + * }; + * + * const result = search( + * 'powertools_json(body)', + * data, + * { customFunctions: new PowertoolsFunctions() } + * ); + * console.log(result); // { foo: 'bar' } + * ``` + * + * When using the {@link extractDataFromEnvelope} function, the PowertoolsFunctions class is automatically used. + * */ class PowertoolsFunctions extends Functions { @Functions.signature({ diff --git a/packages/jmespath/typedoc.json b/packages/jmespath/typedoc.json index 879b1d55e7..737729805a 100644 --- a/packages/jmespath/typedoc.json +++ b/packages/jmespath/typedoc.json @@ -1,7 +1,12 @@ { - "extends": ["../../typedoc.base.json"], + "extends": [ + "../../typedoc.base.json" + ], "entryPoints": [ - "./src/index.ts" + "./src/index.ts", + "./src/types.ts", + "./src/envelopes.ts", + "./src/PowertoolsFunctions.ts", ], "readme": "README.md" } \ No newline at end of file From 68c48124b71fa9d20c97f528a6f872c6e8a72c8b Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 8 Mar 2024 11:03:06 +0100 Subject: [PATCH 085/103] refactor: reduce function cognitive complexity --- packages/commons/src/typeUtils.ts | 75 +++++++++++++++++++------------ 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/packages/commons/src/typeUtils.ts b/packages/commons/src/typeUtils.ts index 7e53de0b1c..75d92a6dfd 100644 --- a/packages/commons/src/typeUtils.ts +++ b/packages/commons/src/typeUtils.ts @@ -99,6 +99,40 @@ const getType = (value: unknown): string => { } }; +/** + * Compare two arrays for strict equality. + * + * @param left The left array to compare + * @param right The right array to compare + */ +const areArraysEqual = (left: unknown[], right: unknown[]): boolean => { + if (left.length !== right.length) { + return false; + } + + return left.every((value, i) => isStrictEqual(value, right[i])); +}; + +/** + * Compare two records for strict equality. + * + * @param left The left record to compare + * @param right The right record to compare + */ +const areRecordsEqual = ( + left: Record, + right: Record +): boolean => { + const leftKeys = Object.keys(left); + const rightKeys = Object.keys(right); + + if (leftKeys.length !== rightKeys.length) { + return false; + } + + return leftKeys.every((key) => isStrictEqual(left[key], right[key])); +}; + /** * Check if two unknown values are strictly equal. * @@ -113,38 +147,21 @@ const getType = (value: unknown): string => { const isStrictEqual = (left: unknown, right: unknown): boolean => { if (left === right) { return true; - } else if (typeof left !== typeof right) { - return false; - } else if (Array.isArray(left) && Array.isArray(right)) { - if (left.length !== right.length) { - return false; - } - for (const [i, value] of left.entries()) { - if (!isStrictEqual(value, right[i])) { - return false; - } - } + } - return true; - } else if (isRecord(left) && isRecord(right)) { - const leftKeys = Object.keys(left); - const leftValues = Object.values(left); - const rightKeys = Object.keys(right); - const rightValues = Object.values(right); - if ( - leftKeys.length !== rightKeys.length || - leftValues.length !== rightValues.length - ) { - return false; - } - - return ( - isStrictEqual(leftKeys, rightKeys) && - isStrictEqual(leftValues, rightValues) - ); - } else { + if (typeof left !== typeof right) { return false; } + + if (Array.isArray(left) && Array.isArray(right)) { + return areArraysEqual(left, right); + } + + if (isRecord(left) && isRecord(right)) { + return areRecordsEqual(left, right); + } + + return false; }; export { From b230b00f8a80cd4c1917ff96a28b7a6be5701db4 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 8 Mar 2024 11:44:54 +0100 Subject: [PATCH 086/103] refactor: reduce class methods cognitive complexity --- packages/jmespath/src/Parser.ts | 519 ++++++++++++++++++++------------ 1 file changed, 332 insertions(+), 187 deletions(-) diff --git a/packages/jmespath/src/Parser.ts b/packages/jmespath/src/Parser.ts index 653b8e30dc..0047de8952 100644 --- a/packages/jmespath/src/Parser.ts +++ b/packages/jmespath/src/Parser.ts @@ -126,12 +126,7 @@ class Parser { this.#index = 0; const parsed = this.#expression(0); if (this.#currentToken() !== 'eof') { - const token = this.#lookaheadToken(0); - throw new ParseError({ - lexPosition: token.start, - tokenValue: token.value, - tokenType: token.type, - }); + this.#throwParseError(); } return new ParsedResult(expression, parsed); @@ -162,186 +157,356 @@ class Parser { */ #getNudFunction(token: Token): Node { const { type: tokenType } = token; - if (tokenType === 'literal') { - return literal(token.value); - } else if (tokenType === 'unquoted_identifier') { - return field(token.value); - } else if (tokenType === 'quoted_identifier') { - const fieldValue = field(token.value); - // You can't have a quoted identifier as a function name - if (this.#currentToken() === 'lparen') { - const token = this.#lookaheadToken(0); - throw new ParseError({ - lexPosition: 0, - tokenValue: token.value, - tokenType: token.type, - reason: 'quoted identifiers cannot be used as a function name', - }); - } + switch (tokenType) { + case 'literal': + return literal(token.value); + case 'unquoted_identifier': + return field(token.value); + case 'quoted_identifier': + return this.#processQuotedIdentifier(token); + case 'star': + return this.#processStarToken(); + case 'filter': + return this.#getLedFunction(tokenType, identity()); + case 'lbrace': + return this.#parseMultiSelectHash(); + case 'lparen': + return this.#processLParenTokenNud(); + case 'flatten': + return this.#processFlattenTokenNud(); + case 'not': + return notExpression(this.#expression(BINDING_POWER['not'])); + case 'lbracket': + return this.#processLBracketTokenNud(); + case 'current': + return currentNode(); + case 'expref': + return expref(this.#expression(BINDING_POWER['expref'])); + default: + return this.#processDefaultToken(token); + } + } - return fieldValue; - } else if (tokenType === 'star') { - const left = identity(); - let right; - if (this.#currentToken() === 'rbracket') { - right = identity(); - } else { - right = this.#parseProjectionRhs(BINDING_POWER['star']); - } + /** + * Process a quoted identifier. + * + * A quoted identifier is a string that is enclosed in double quotes. + * + * @example s."foo" + * + * @param token The token to process + */ + #processQuotedIdentifier(token: Token): Node { + const fieldValue = field(token.value); + if (this.#currentToken() === 'lparen') { + this.#throwParseError({ + lexPosition: 0, + reason: 'quoted identifiers cannot be used as a function name', + }); + } - return valueProjection(left, right); - } else if (tokenType === 'filter') { - return this.#getLedFunction(tokenType, identity()); - } else if (tokenType === 'lbrace') { - return this.#parseMultiSelectHash(); - } else if (tokenType === 'lparen') { - const expression = this.#expression(); - this.#match('rparen'); - - return expression; - } else if (tokenType === 'flatten') { - const left = flatten(identity()); - const right = this.#parseProjectionRhs(BINDING_POWER['flatten']); - - return projection(left, right); - } else if (tokenType === 'not') { - const expression = this.#expression(BINDING_POWER['not']); - - return notExpression(expression); - } else if (tokenType === 'lbracket') { - if (['number', 'colon'].includes(this.#currentToken())) { - const right = this.#parseIndexExpression(); - // We could optimize this and remove the identity() node - // We don't really need an indexExpression node, we can - // just emit an index node here if we're not dealing - // with a slice. - - return this.#projectIfSlice(identity(), right); - } else if ( - this.#currentToken() === 'star' && - this.#lookahead(1) === 'rbracket' - ) { - this.#advance(); - this.#advance(); - const right = this.#parseProjectionRhs(BINDING_POWER['star']); + return fieldValue; + } - return projection(identity(), right); - } else { - return this.#parseMultiSelectList(); - } - } else if (tokenType === 'current') { - return currentNode(); - } else if (tokenType === 'expref') { - return expref(this.#expression(BINDING_POWER['expref'])); + /** + * Process a star token. + * + * A star token is a syntax that allows you to project all the + * elements in a list or dictionary. + * + * @example foo[*] + */ + #processStarToken(): Node { + const left = identity(); + let right; + if (this.#currentToken() === 'rbracket') { + right = identity(); } else { - if (tokenType === 'eof') { - throw new IncompleteExpressionError({ - lexPosition: token.start, - tokenValue: token.value, - tokenType: token.type, - }); - } + right = this.#parseProjectionRhs(BINDING_POWER['star']); + } - throw new ParseError({ + return valueProjection(left, right); + } + + /** + * Process a left parenthesis token. + * + * A left parenthesis token is a syntax that allows you to group + * expressions together. + * + * @example (foo.bar) + */ + #processLParenTokenNud(): Node { + const expression = this.#expression(); + this.#match('rparen'); + + return expression; + } + + /** + * Process a flatten token. + * + * A flatten token is a syntax that allows you to flatten the + * results of a subexpression. + * + * @example foo[].bar + */ + #processFlattenTokenNud(): Node { + const left = flatten(identity()); + const right = this.#parseProjectionRhs(BINDING_POWER['flatten']); + + return projection(left, right); + } + + /** + * Process a left bracket token. + * + * A left bracket token is a syntax that allows you to access + * elements in a list or dictionary. + * + * @example foo[0] + */ + #processLBracketTokenNud(): Node { + if (['number', 'colon'].includes(this.#currentToken())) { + const right = this.#parseIndexExpression(); + + return this.#projectIfSlice(identity(), right); + } else if ( + this.#currentToken() === 'star' && + this.#lookahead(1) === 'rbracket' + ) { + this.#advance(); + this.#advance(); + const right = this.#parseProjectionRhs(BINDING_POWER['star']); + + return projection(identity(), right); + } else { + return this.#parseMultiSelectList(); + } + } + + /** + * Process a default token. + * + * A default token is a syntax that allows you to access + * elements in a list or dictionary. + * + * @param token The token to process + */ + #processDefaultToken(token: Token): Node { + if (token.type === 'eof') { + throw new IncompleteExpressionError({ lexPosition: token.start, tokenValue: token.value, tokenType: token.type, }); } + throw new ParseError({ + lexPosition: token.start, + tokenValue: token.value, + tokenType: token.type, + }); } + /** + * Get the led function for a token. This is the function that + * is called when a token is found in the middle of an expression. + * + * @param tokenType The type of token to get the led function for. + * @param leftNode The left hand side of the expression. + */ #getLedFunction(tokenType: Token['type'], leftNode: Node): Node { - if (tokenType === 'dot') { - if (this.#currentToken() !== 'star') { - const right = this.#parseDotRhs(BINDING_POWER[tokenType]); - if (leftNode.type === 'subexpression') { - leftNode.children.push(right); - - return leftNode; - } else { - return subexpression([leftNode, right]); - } - } else { - // We are creating a value projection - this.#advance(); - const right = this.#parseProjectionRhs(BINDING_POWER[tokenType]); + switch (tokenType) { + case 'dot': + return this.#processDotToken(leftNode); + case 'pipe': + return this.#processPipeToken(leftNode); + case 'or': + return this.#processOrToken(leftNode); + case 'and': + return this.#processAndToken(leftNode); + case 'lparen': + return this.#processLParenToken(leftNode); + case 'filter': + return this.#processFilterToken(leftNode); + case 'eq': + case 'ne': + case 'gt': + case 'gte': + case 'lt': + case 'lte': + return this.#parseComparator(leftNode, tokenType); + case 'flatten': + return this.#processFlattenToken(leftNode); + case 'lbracket': + return this.#processLBracketToken(leftNode); + default: + return this.#throwParseError(); + } + } - return valueProjection(leftNode, right); - } - } else if (tokenType === 'pipe') { - const right = this.#expression(BINDING_POWER[tokenType]); - - return pipe(leftNode, right); - } else if (tokenType === 'or') { - const right = this.#expression(BINDING_POWER[tokenType]); - - return orExpression(leftNode, right); - } else if (tokenType === 'and') { - const right = this.#expression(BINDING_POWER[tokenType]); - - return andExpression(leftNode, right); - } else if (tokenType === 'lparen') { - const name = leftNode.value as string; - const args = []; - while (this.#currentToken() !== 'rparen') { - const expression = this.#expression(); - if (this.#currentToken() === 'comma') { - this.#match('comma'); - } - args.push(expression); - } - this.#match('rparen'); + /** + * Process a dot token. + * + * A dot token is a syntax that allows you to access + * fields in a dictionary or elements in a list. + * + * @example foo.bar + * + * @param leftNode The left hand side of the expression. + */ + #processDotToken(leftNode: Node): Node { + if (this.#currentToken() !== 'star') { + const right = this.#parseDotRhs(BINDING_POWER['dot']); + if (leftNode.type === 'subexpression') { + leftNode.children.push(right); - return functionExpression(name, args); - } else if (tokenType === 'filter') { - // Filters are projections - const condition = this.#expression(0); - this.#match('rbracket'); - let right: Node; - if (this.#currentToken() === 'flatten') { - right = identity(); + return leftNode; } else { - right = this.#parseProjectionRhs(BINDING_POWER['flatten']); + return subexpression([leftNode, right]); } + } else { + // We are creating a value projection + this.#advance(); + const right = this.#parseProjectionRhs(BINDING_POWER['dot']); + + return valueProjection(leftNode, right); + } + } - return filterProjection(leftNode, right, condition); - } else if (['eq', 'ne', 'gt', 'gte', 'lt', 'lte'].includes(tokenType)) { - return this.#parseComparator(leftNode, tokenType); - } else if (tokenType === 'flatten') { - const left = flatten(leftNode); - const right = this.#parseProjectionRhs(BINDING_POWER['flatten']); + /** + * Process a pipe token. + * + * A pipe token is a syntax that allows you to combine two + * expressions using the pipe operator. + * + * @example foo | bar + * + * @param leftNode The left hand side of the expression. + */ + #processPipeToken(leftNode: Node): Node { + const right = this.#expression(BINDING_POWER['pipe']); - return projection(left, right); - } else if (tokenType === 'lbracket') { - const token = this.#lookaheadToken(0); - if (['number', 'colon'].includes(token.type)) { - const right = this.#parseIndexExpression(); - if (leftNode.type === 'index_expression') { - // Optimization: if the left node is an index expression - // we can avoid creating another node and instead just - // add the right node as a child of the left node. - leftNode.children.push(right); - - return leftNode; - } else { - return this.#projectIfSlice(leftNode, right); - } - } else { - // We have a projection - this.#match('star'); - this.#match('rbracket'); - const right = this.#parseProjectionRhs(BINDING_POWER['star']); + return pipe(leftNode, right); + } - return projection(leftNode, right); + /** + * Process an or token. + * + * An or token is a syntax that allows you to combine two + * expressions using the logical or operator. + * + * @example foo || bar + * + * @param leftNode The left hand side of the expression. + */ + #processOrToken(leftNode: Node): Node { + const right = this.#expression(BINDING_POWER['or']); + + return orExpression(leftNode, right); + } + + /** + * Process an and token. + * + * An and token is a syntax that allows you to combine two + * expressions using the logical and operator. + * + * @example foo && bar + * + * @param leftNode The left hand side of the expression. + */ + #processAndToken(leftNode: Node): Node { + const right = this.#expression(BINDING_POWER['and']); + + return andExpression(leftNode, right); + } + + #processLParenToken(leftNode: Node): Node { + const name = leftNode.value as string; + const args = []; + while (this.#currentToken() !== 'rparen') { + const expression = this.#expression(); + if (this.#currentToken() === 'comma') { + this.#match('comma'); } + args.push(expression); + } + this.#match('rparen'); + + return functionExpression(name, args); + } + + #processFilterToken(leftNode: Node): Node { + // Filters are projections + const condition = this.#expression(0); + this.#match('rbracket'); + let right: Node; + if (this.#currentToken() === 'flatten') { + right = identity(); } else { - const token = this.#lookaheadToken(0); - throw new ParseError({ - lexPosition: token.start, - tokenValue: token.value, - tokenType: token.type, - }); + right = this.#parseProjectionRhs(BINDING_POWER['flatten']); } + + return filterProjection(leftNode, right, condition); + } + + #processFlattenToken(leftNode: Node): Node { + const left = flatten(leftNode); + const right = this.#parseProjectionRhs(BINDING_POWER['flatten']); + + return projection(left, right); + } + + #processLBracketToken(leftNode: Node): Node { + const token = this.#lookaheadToken(0); + if (['number', 'colon'].includes(token.type)) { + const right = this.#parseIndexExpression(); + if (leftNode.type === 'index_expression') { + // Optimization: if the left node is an index expression + // we can avoid creating another node and instead just + // add the right node as a child of the left node. + leftNode.children.push(right); + + return leftNode; + } else { + return this.#projectIfSlice(leftNode, right); + } + } else { + // We have a projection + this.#match('star'); + this.#match('rbracket'); + const right = this.#parseProjectionRhs(BINDING_POWER['star']); + + return projection(leftNode, right); + } + } + + /** + * Throw a parse error. + * + * This type of error indicates that the parser encountered + * a syntax error while processing the expression. + * + * The error includes the position in the expression where + * the error occurred, the value of the token that caused + * the error, the type of the token, and an optional reason. + * + * @param options The options to use when throwing the error. + */ + #throwParseError(options?: { + lexPosition?: number; + tokenValue?: Token['value']; + tokenType?: Token['type']; + reason?: string; + }): never { + const token = this.#lookaheadToken(0); + throw new ParseError({ + lexPosition: options?.lexPosition ?? token.start, + tokenValue: options?.tokenValue ?? token.value, + tokenType: options?.tokenType ?? token.type, + reason: options?.reason, + }); } /** @@ -398,24 +563,14 @@ class Parser { if (currentToken === 'colon') { index += 1; if (index === 3) { - const token = this.#lookaheadToken(0); - throw new ParseError({ - lexPosition: token.start, - tokenValue: token.value, - tokenType: token.type, - }); + this.#throwParseError(); } this.#advance(); } else if (currentToken === 'number') { parts[index] = this.#lookaheadToken(0).value; this.#advance(); } else { - const token = this.#lookaheadToken(0); - throw new ParseError({ - lexPosition: token.start, - tokenValue: token.value, - tokenType: token.type, - }); + this.#throwParseError(); } currentToken = this.#currentToken(); } @@ -534,12 +689,7 @@ class Parser { this.#match('dot'); right = this.#parseDotRhs(bindingPower); } else { - const token = this.#lookaheadToken(0); - throw new ParseError({ - lexPosition: token.start, - tokenValue: token.value, - tokenType: token.type, - }); + this.#throwParseError(); } return right; @@ -574,12 +724,7 @@ class Parser { return this.#parseMultiSelectHash(); } else { - const token = this.#lookaheadToken(0); - throw new ParseError({ - lexPosition: token.start, - tokenValue: token.value, - tokenType: token.type, - }); + this.#throwParseError(); } } From eb235c6e77eeecd095e966478ab32953c7af5aa5 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 8 Mar 2024 11:51:49 +0100 Subject: [PATCH 087/103] refactor: reduce class methods cognitive complexity --- packages/jmespath/src/Lexer.ts | 17 ++-- packages/jmespath/src/TreeInterpreter.ts | 113 +++++++-------------- packages/jmespath/src/utils.ts | 73 +++++++------ packages/jmespath/tests/unit/index.test.ts | 8 ++ 4 files changed, 93 insertions(+), 118 deletions(-) diff --git a/packages/jmespath/src/Lexer.ts b/packages/jmespath/src/Lexer.ts index d2a4994f70..db6bf41ab8 100644 --- a/packages/jmespath/src/Lexer.ts +++ b/packages/jmespath/src/Lexer.ts @@ -89,12 +89,17 @@ class Lexer { // Negative number. const start = this.#position; const buff = this.#consumeNumber(); - yield { - type: 'number', - value: parseInt(buff), - start: start, - end: start + buff.length, - }; + if (buff.length > 1) { + yield { + type: 'number', + value: parseInt(buff), + start: start, + end: start + buff.length, + }; + } else { + // If the negative sign is not followed by a number, it is an error. + throw new LexerError(start, 'Unknown token after "-"'); + } } else if (this.#current === '"') { yield this.#consumeQuotedIdentifier(); } else if (this.#current === '<') { diff --git a/packages/jmespath/src/TreeInterpreter.ts b/packages/jmespath/src/TreeInterpreter.ts index 55ab0673fc..b9900562f8 100644 --- a/packages/jmespath/src/TreeInterpreter.ts +++ b/packages/jmespath/src/TreeInterpreter.ts @@ -49,60 +49,36 @@ class TreeInterpreter { */ public visit(node: Node, value: JSONObject): JSONObject | null { const nodeType = node.type; - if (nodeType === 'subexpression') { - return this.#visitSubexpression(node, value); - } else if (nodeType === 'field') { - return this.#visitField(node, value); - } else if (nodeType === 'comparator') { - return this.#visitComparator(node, value); - } else if (nodeType === 'current') { - return this.#visitCurrent(node, value); - } else if (nodeType === 'expref') { - // This is a special case where we return an instance of the Expression - // class instead of the result of visiting the node. This is because the - // expref node represents a reference to another expression, so we want - // to return an instance of the Expression class so that we can evaluate - // the referenced expression later. Adding `Expression` to the return - // type of the `visit` method would mean having to type-check the return - // type of every call to `visit` in the interpreter even though we only - // return an instance of `Expression` in this one case. - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-next-line - return this.#visitExpref(node, value); - } else if (nodeType === 'function_expression') { - return this.#visitFunctionExpression(node, value); - } else if (nodeType === 'filter_projection') { - return this.#visitFilterProjection(node, value); - } else if (nodeType === 'flatten') { - return this.#visitFlatten(node, value); - } else if (nodeType === 'identity') { - return this.#visitIdentity(node, value); - } else if (nodeType === 'index') { - return this.#visitIndex(node, value); - } else if (nodeType === 'index_expression') { - return this.#visitIndexExpression(node, value); - } else if (nodeType === 'slice') { - return this.#visitSlice(node, value); - } else if (nodeType === 'key_val_pair') { - return this.#visitKeyValPair(node, value); - } else if (nodeType === 'literal') { - return this.#visitLiteral(node, value); - } else if (nodeType === 'multi_select_object') { - return this.#visitMultiSelectObject(node, value); - } else if (nodeType === 'multi_select_list') { - return this.#visitMultiSelectList(node, value); - } else if (nodeType === 'or_expression') { - return this.#visitOrExpression(node, value); - } else if (nodeType === 'and_expression') { - return this.#visitAndExpression(node, value); - } else if (nodeType === 'not_expression') { - return this.#visitNotExpression(node, value); - } else if (nodeType === 'pipe') { - return this.#visitPipe(node, value); - } else if (nodeType === 'projection') { - return this.#visitProjection(node, value); - } else if (nodeType === 'value_projection') { - return this.#visitValueProjection(node, value); + const visitMethods: { + [key: string]: (node: Node, value: JSONObject) => JSONObject | null; + } = { + subexpression: this.#visitSubexpressionOrIndexExpression, + field: this.#visitField, + comparator: this.#visitComparator, + current: this.#visitCurrent, + expref: this.#visitExpref, + function_expression: this.#visitFunctionExpression, + filter_projection: this.#visitFilterProjection, + flatten: this.#visitFlatten, + identity: this.#visitIdentity, + index: this.#visitIndex, + index_expression: this.#visitSubexpressionOrIndexExpression, + slice: this.#visitSlice, + key_val_pair: this.#visitKeyValPair, + literal: this.#visitLiteral, + multi_select_object: this.#visitMultiSelectObject, + multi_select_list: this.#visitMultiSelectList, + or_expression: this.#visitOrExpression, + and_expression: this.#visitAndExpression, + not_expression: this.#visitNotExpression, + pipe: this.#visitPipe, + projection: this.#visitProjection, + value_projection: this.#visitValueProjection, + }; + + const visitMethod = visitMethods[nodeType]; + if (visitMethod) { + return visitMethod.call(this, node, value); } else { throw new JMESPathError( `Not Implemented: Invalid node type: ${node.type}` @@ -111,12 +87,17 @@ class TreeInterpreter { } /** - * Visit a subexpression node. + * Visit a subexpression or index expression node. + * + * This method is shared between subexpression and index expression nodes. * * @param node The subexpression node to visit. * @param value The current value to visit. */ - #visitSubexpression(node: Node, value: JSONObject): JSONObject { + #visitSubexpressionOrIndexExpression( + node: Node, + value: JSONObject + ): JSONObject { let result = value; for (const child of node.children) { result = this.visit(child, result); @@ -124,7 +105,6 @@ class TreeInterpreter { return result; } - /** * Visit a field node. * @@ -333,21 +313,6 @@ class TreeInterpreter { return found; } - /** - * Visit an index expression node. - * - * @param node The index expression node to visit. - * @param value The current value to visit. - */ - #visitIndexExpression(node: Node, value: JSONObject): JSONObject { - let result = value; - for (const child of node.children) { - result = this.visit(child, result); - } - - return result; - } - /** * Visit a slice node. * @@ -527,10 +492,10 @@ class TreeInterpreter { if (!isRecord(base)) { return null; } - const values = Object.values(base) as JSONObject[]; + const values = Object.values(base); const collected = []; for (const item of values) { - const current = this.visit(node.children[1], item); + const current = this.visit(node.children[1], item as JSONObject[]); if (current !== null) { collected.push(current); } diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts index 329f66bfc7..77936fc625 100644 --- a/packages/jmespath/src/utils.ts +++ b/packages/jmespath/src/utils.ts @@ -86,18 +86,16 @@ const sliceArray = ({ }): T[] | null => { const isStepNegative = step < 0; const length = array.length; + const defaultStart = isStepNegative ? length - 1 : 0; + const defaultEnd = isStepNegative ? -1 : length; start = isIntegerNumber(start) ? capSliceRange(length, start, isStepNegative) - : isStepNegative - ? length - 1 - : 0; + : defaultStart; end = isIntegerNumber(end) ? capSliceRange(length, end, isStepNegative) - : isStepNegative - ? -1 - : length; + : defaultEnd; const result: T[] = []; if (step > 0) { @@ -214,42 +212,41 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { } } break; - } else { - if (type === 'expression') { - if (!(arg instanceof Expression)) { - if (!hasMoreTypesToCheck) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } + } + if (type === 'expression') { + if (!(arg instanceof Expression)) { + if (!hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); } - break; - } else if (type === 'string' || type === 'number' || type === 'boolean') { - if (typeof arg !== type) { - if (!hasMoreTypesToCheck) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - continue; + } + break; + } else if (type === 'string' || type === 'number' || type === 'boolean') { + if (typeof arg !== type) { + if (!hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); } - break; - } else if (type === 'object') { - if (!isRecord(arg)) { - if (index === entryCount - 1) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } + continue; + } + break; + } else if (type === 'object') { + if (!isRecord(arg)) { + if (index === entryCount - 1) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); } - break; } + break; } } }; diff --git a/packages/jmespath/tests/unit/index.test.ts b/packages/jmespath/tests/unit/index.test.ts index 052fcd7c07..04d1019453 100644 --- a/packages/jmespath/tests/unit/index.test.ts +++ b/packages/jmespath/tests/unit/index.test.ts @@ -22,6 +22,14 @@ import { extractDataFromEnvelope, SQS } from '../../src/envelopes.js'; describe('Coverage tests', () => { // These expressions tests are not part of the compliance suite, but are added to ensure coverage describe('expressions', () => { + it('throws an error if the index is an invalid value', () => { + // Prepare + const invalidIndexExpression = 'foo.*.notbaz[-a]'; + + // Act & Assess + expect(() => search(invalidIndexExpression, {})).toThrow(LexerError); + }); + it('throws an error if the expression is not a string', () => { // Prepare const notAStringExpression = 3; From 48e01f130cbed8efa0eebfa70071617ed20a5f87 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 8 Mar 2024 12:03:37 +0100 Subject: [PATCH 088/103] refactor: reduce class methods cognitive complexity --- packages/jmespath/src/TreeInterpreter.ts | 2 +- packages/jmespath/src/errors.ts | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/jmespath/src/TreeInterpreter.ts b/packages/jmespath/src/TreeInterpreter.ts index b9900562f8..e45d44f5ac 100644 --- a/packages/jmespath/src/TreeInterpreter.ts +++ b/packages/jmespath/src/TreeInterpreter.ts @@ -225,7 +225,7 @@ class TreeInterpreter { error instanceof VariadicArityError || error instanceof ArityError ) { - error.setFunctionName(node.value); + error.setEvaluatedFunctionName(node.value); throw error; } } diff --git a/packages/jmespath/src/errors.ts b/packages/jmespath/src/errors.ts index 4d56fd96aa..c0d7112539 100644 --- a/packages/jmespath/src/errors.ts +++ b/packages/jmespath/src/errors.ts @@ -92,11 +92,14 @@ class ParseError extends JMESPathError { this.reason = options.reason; // Set the message to include the lexer position and token info. - const issue = this.reason - ? this.reason - : this.tokenType === 'eof' - ? 'found unexpected end of expression (EOF)' - : `found unexpected token "${this.tokenValue}" (${this.tokenType})`; + let issue: string; + if (this.reason) { + issue = this.reason; + } else if (this.tokenType === 'eof') { + issue = 'found unexpected end of expression (EOF)'; + } else { + issue = `found unexpected token "${this.tokenValue}" (${this.tokenType})`; + } this.message = `${this.message}: parse error at column ${this.lexPosition}, ${issue}`; } } @@ -169,7 +172,7 @@ class FunctionError extends JMESPathError { * * @param functionName The function that was being validated or executed when the error occurred. */ - public setFunctionName(functionName: string): void { + public setEvaluatedFunctionName(functionName: string): void { this.message = this.message.replace( 'for function undefined', `for function ${functionName}()` @@ -267,9 +270,12 @@ class JMESPathTypeError extends FunctionError { } protected serializeExpectedTypes(): string { - return this.expectedTypes.length === 1 - ? `"${this.expectedTypes[0]}"` - : `one of ${this.expectedTypes.map((type) => `"${type}"`).join(', ')}`; + const types: string[] = []; + for (const type of this.expectedTypes) { + types.push(`"${type}"`); + } + + return types.length === 1 ? types[0] : `one of ${types.join(', ')}`; } } From 52eb0e3a8ef7e1351581a87f84e2b63b00d3aa84 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 8 Mar 2024 17:06:34 +0100 Subject: [PATCH 089/103] refactor: reduce code complexity --- packages/jmespath/src/TreeInterpreter.ts | 32 ++++++++---------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/packages/jmespath/src/TreeInterpreter.ts b/packages/jmespath/src/TreeInterpreter.ts index e45d44f5ac..28d2c531bc 100644 --- a/packages/jmespath/src/TreeInterpreter.ts +++ b/packages/jmespath/src/TreeInterpreter.ts @@ -52,7 +52,7 @@ class TreeInterpreter { const visitMethods: { [key: string]: (node: Node, value: JSONObject) => JSONObject | null; } = { - subexpression: this.#visitSubexpressionOrIndexExpression, + subexpression: this.#visitSubexpressionOrIndexExpressionOrPipe, field: this.#visitField, comparator: this.#visitComparator, current: this.#visitCurrent, @@ -62,7 +62,7 @@ class TreeInterpreter { flatten: this.#visitFlatten, identity: this.#visitIdentity, index: this.#visitIndex, - index_expression: this.#visitSubexpressionOrIndexExpression, + index_expression: this.#visitSubexpressionOrIndexExpressionOrPipe, slice: this.#visitSlice, key_val_pair: this.#visitKeyValPair, literal: this.#visitLiteral, @@ -71,7 +71,7 @@ class TreeInterpreter { or_expression: this.#visitOrExpression, and_expression: this.#visitAndExpression, not_expression: this.#visitNotExpression, - pipe: this.#visitPipe, + pipe: this.#visitSubexpressionOrIndexExpressionOrPipe, projection: this.#visitProjection, value_projection: this.#visitValueProjection, }; @@ -87,14 +87,17 @@ class TreeInterpreter { } /** - * Visit a subexpression or index expression node. + * Visit a subexpression, index expression, or pipe node. * - * This method is shared between subexpression and index expression nodes. + * This method is shared between subexpression, index expression, and pipe + * since they all behave the same way in the context of an expression. * - * @param node The subexpression node to visit. + * They all visit their children and return the result of the last child. + * + * @param node The node to visit. * @param value The current value to visit. */ - #visitSubexpressionOrIndexExpression( + #visitSubexpressionOrIndexExpressionOrPipe( node: Node, value: JSONObject ): JSONObject { @@ -444,21 +447,6 @@ class TreeInterpreter { return !isTruthy(originalResult); } - /** - * Visit a pipe node. - * - * @param node The pipe node to visit. - * @param value The current value to visit. - */ - #visitPipe(node: Node, value: JSONObject): JSONObject { - let result = value; - for (const child of node.children) { - result = this.visit(child, result); - } - - return result; - } - /** * Visit a projection node. * From 04c3e065b75cffe9a549b2f22fbfb9f3fc184610 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 17:49:43 +0100 Subject: [PATCH 090/103] chore: consume eq sign --- packages/jmespath/src/Lexer.ts | 34 +++++++++++++------ .../tests/unit/compliance/syntax.test.ts | 4 +-- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/jmespath/src/Lexer.ts b/packages/jmespath/src/Lexer.ts index db6bf41ab8..0482ea96a4 100644 --- a/packages/jmespath/src/Lexer.ts +++ b/packages/jmespath/src/Lexer.ts @@ -109,17 +109,7 @@ class Lexer { } else if (this.#current === '!') { yield this.#matchOrElse('=', 'ne', 'not'); } else if (this.#current === '=') { - if (this.#next() === '=') { - yield { - type: 'eq', - value: '==', - start: this.#position - 1, - end: this.#position, - }; - this.#next(); - } else { - throw new LexerError(this.#position - 1, '='); - } + yield this.#consumeEqualSign(); } else { throw new LexerError(this.#position, this.#current); } @@ -127,6 +117,28 @@ class Lexer { yield { type: 'eof', value: '', start: this.#length, end: this.#length }; } + /** + * Consume an equal sign. + * + * This method is called when the lexer encounters an equal sign. + * It checks if the next character is also an equal sign and returns + * the corresponding token. + */ + #consumeEqualSign(): Token { + if (this.#next() === '=') { + this.#next(); + + return { + type: 'eq', + value: '==', + start: this.#position - 1, + end: this.#position, + }; + } else { + throw new LexerError(this.#position - 1, '='); + } + } + /** * Consume a raw string that is a number. * diff --git a/packages/jmespath/tests/unit/compliance/syntax.test.ts b/packages/jmespath/tests/unit/compliance/syntax.test.ts index a27203504b..96a7e9fee8 100644 --- a/packages/jmespath/tests/unit/compliance/syntax.test.ts +++ b/packages/jmespath/tests/unit/compliance/syntax.test.ts @@ -730,12 +730,12 @@ describe('Syntax tests', () => { { expression: 'foo[?==]', error: - 'Invalid jmespath expression: parse error at column 5, found unexpected token "==" (eq) in expression: foo[?==]', + 'Invalid jmespath expression: parse error at column 6, found unexpected token "==" (eq) in expression: foo[?==]', }, { expression: 'foo[?==bar]', error: - 'Invalid jmespath expression: parse error at column 5, found unexpected token "==" (eq) in expression: foo[?==bar]', + 'Invalid jmespath expression: parse error at column 6, found unexpected token "==" (eq) in expression: foo[?==bar]', }, { expression: 'foo[?bar==baz?]', From db19a63dec5b402267e912e72a75302078725d62 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 17:51:35 +0100 Subject: [PATCH 091/103] chore: consume negative number --- packages/jmespath/src/Lexer.ts | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/jmespath/src/Lexer.ts b/packages/jmespath/src/Lexer.ts index 0482ea96a4..6499e276d8 100644 --- a/packages/jmespath/src/Lexer.ts +++ b/packages/jmespath/src/Lexer.ts @@ -86,20 +86,7 @@ class Lexer { end: start + buff.length, }; } else if (this.#current === '-') { - // Negative number. - const start = this.#position; - const buff = this.#consumeNumber(); - if (buff.length > 1) { - yield { - type: 'number', - value: parseInt(buff), - start: start, - end: start + buff.length, - }; - } else { - // If the negative sign is not followed by a number, it is an error. - throw new LexerError(start, 'Unknown token after "-"'); - } + yield this.#consumeNegativeNumber(); } else if (this.#current === '"') { yield this.#consumeQuotedIdentifier(); } else if (this.#current === '<') { @@ -139,6 +126,22 @@ class Lexer { } } + #consumeNegativeNumber(): Token { + const start = this.#position; + const buff = this.#consumeNumber(); + if (buff.length > 1) { + return { + type: 'number', + value: parseInt(buff), + start: start, + end: start + buff.length, + }; + } else { + // If the negative sign is not followed by a number, it is an error. + throw new LexerError(start, 'Unknown token after "-"'); + } + } + /** * Consume a raw string that is a number. * From c0c93f224411ebe2c24d954143f2b27023f1f7eb Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 17:53:56 +0100 Subject: [PATCH 092/103] chore: consume sq bracket --- packages/jmespath/src/Lexer.ts | 41 +++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/jmespath/src/Lexer.ts b/packages/jmespath/src/Lexer.ts index 6499e276d8..3faf7ddbf3 100644 --- a/packages/jmespath/src/Lexer.ts +++ b/packages/jmespath/src/Lexer.ts @@ -57,17 +57,7 @@ class Lexer { } else if (WHITESPACE.has(this.#current)) { this.#next(); } else if (this.#current === '[') { - const start = this.#position; - const nextChar = this.#next(); - if (nextChar == ']') { - this.#next(); - yield { type: 'flatten', value: '[]', start: start, end: start + 2 }; - } else if (nextChar == '?') { - this.#next(); - yield { type: 'filter', value: '[?', start: start, end: start + 2 }; - } else { - yield { type: 'lbracket', value: '[', start: start, end: start + 1 }; - } + yield this.#consumeSquareBracket(); } else if (this.#current === `'`) { yield this.#consumeRawStringLiteral(); } else if (this.#current === '|') { @@ -126,6 +116,12 @@ class Lexer { } } + /** + * Consume a negative number. + * + * This method is called when the lexer encounters a negative sign. + * It checks if the next character is a number and returns the corresponding token. + */ #consumeNegativeNumber(): Token { const start = this.#position; const buff = this.#consumeNumber(); @@ -158,6 +154,29 @@ class Lexer { return buff; } + /** + * Consume a square bracket. + * + * This method is called when the lexer encounters a square bracket. + * It checks if the next character is a question mark or a closing + * square bracket and returns the corresponding token. + */ + #consumeSquareBracket(): Token { + const start = this.#position; + const nextChar = this.#next(); + if (nextChar == ']') { + this.#next(); + + return { type: 'flatten', value: '[]', start: start, end: start + 2 }; + } else if (nextChar == '?') { + this.#next(); + + return { type: 'filter', value: '[?', start: start, end: start + 2 }; + } else { + return { type: 'lbracket', value: '[', start: start, end: start + 1 }; + } + } + /** * Initializes the lexer for the given expression. * From 4975d1077b82e399d33cbb541b61aff18d799def Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 17:54:50 +0100 Subject: [PATCH 093/103] chore: consume identifier --- packages/jmespath/src/Lexer.ts | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/jmespath/src/Lexer.ts b/packages/jmespath/src/Lexer.ts index 3faf7ddbf3..69b1448181 100644 --- a/packages/jmespath/src/Lexer.ts +++ b/packages/jmespath/src/Lexer.ts @@ -43,17 +43,7 @@ class Lexer { this.#next(); } else if (START_IDENTIFIER.has(this.#current)) { - const start = this.#position; - let buff = this.#current; - while (VALID_IDENTIFIER.has(this.#next())) { - buff += this.#current; - } - yield { - type: 'unquoted_identifier', - value: buff, - start, - end: start + buff.length, - }; + yield this.#consumeIdentifier(); } else if (WHITESPACE.has(this.#current)) { this.#next(); } else if (this.#current === '[') { @@ -116,6 +106,28 @@ class Lexer { } } + /** + * Consume an unquoted identifier. + * + * This method is called when the lexer encounters a character that is a valid + * identifier. It advances the lexer until it finds a character that is not a + * valid identifier and returns the corresponding token. + */ + #consumeIdentifier(): Token { + const start = this.#position; + let buff = this.#current; + while (VALID_IDENTIFIER.has(this.#next())) { + buff += this.#current; + } + + return { + type: 'unquoted_identifier', + value: buff, + start, + end: start + buff.length, + }; + } + /** * Consume a negative number. * From 3439f3bc17f105a8c5c21186e803b7f05f115ef4 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 18:07:18 +0100 Subject: [PATCH 094/103] chore: reduce complexity --- packages/jmespath/src/Lexer.ts | 42 ++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/packages/jmespath/src/Lexer.ts b/packages/jmespath/src/Lexer.ts index 69b1448181..e4cdf1aaa2 100644 --- a/packages/jmespath/src/Lexer.ts +++ b/packages/jmespath/src/Lexer.ts @@ -50,10 +50,6 @@ class Lexer { yield this.#consumeSquareBracket(); } else if (this.#current === `'`) { yield this.#consumeRawStringLiteral(); - } else if (this.#current === '|') { - yield this.#matchOrElse('|', 'or', 'pipe'); - } else if (this.#current === '&') { - yield this.#matchOrElse('&', 'and', 'expref'); } else if (this.#current === '`') { yield this.#consumeLiteral(); } else if (VALID_NUMBER.has(this.#current)) { @@ -69,14 +65,10 @@ class Lexer { yield this.#consumeNegativeNumber(); } else if (this.#current === '"') { yield this.#consumeQuotedIdentifier(); - } else if (this.#current === '<') { - yield this.#matchOrElse('=', 'lte', 'lt'); - } else if (this.#current === '>') { - yield this.#matchOrElse('=', 'gte', 'gt'); - } else if (this.#current === '!') { - yield this.#matchOrElse('=', 'ne', 'not'); - } else if (this.#current === '=') { - yield this.#consumeEqualSign(); + } else if (['<', '>', '!', '=', '|', '&'].includes(this.#current)) { + yield this.#consumeComparatorSigns( + this.#current as '<' | '>' | '!' | '=' | '|' | '&' + ); } else { throw new LexerError(this.#position, this.#current); } @@ -84,6 +76,32 @@ class Lexer { yield { type: 'eof', value: '', start: this.#length, end: this.#length }; } + /** + * Consume a comparator sign. + * + * This method is called when the lexer encounters a comparator sign. + * + * @param current The current character + */ + #consumeComparatorSigns = ( + current: '<' | '>' | '!' | '=' | '|' | '&' + ): Token => { + switch (current) { + case '<': + return this.#matchOrElse('=', 'lte', 'lt'); + case '>': + return this.#matchOrElse('=', 'gte', 'gt'); + case '!': + return this.#matchOrElse('=', 'ne', 'not'); + case '|': + return this.#matchOrElse('|', 'or', 'pipe'); + case '&': + return this.#matchOrElse('&', 'and', 'expref'); + default: + return this.#consumeEqualSign(); + } + }; + /** * Consume an equal sign. * From 79bd5f6945ac1a7a5b333dbcccf2fcab5acfa003 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 18:18:22 +0100 Subject: [PATCH 095/103] chore: reduce complexity --- packages/jmespath/src/utils.ts | 109 +++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 46 deletions(-) diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts index 77936fc625..e787652bb3 100644 --- a/packages/jmespath/src/utils.ts +++ b/packages/jmespath/src/utils.ts @@ -154,9 +154,10 @@ const typeCheck = ( args: unknown[], argumentsSpecs: Array> ): void => { - argumentsSpecs.forEach((argumentSpec, index) => { + for (const [index, argumentSpec] of argumentsSpecs.entries()) { + if (argumentSpec[0] === 'any') continue; typeCheckArgument(args[index], argumentSpec); - }); + } }; /** @@ -179,9 +180,6 @@ const typeCheck = ( * @param argumentSpec */ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { - if (argumentSpec.length === 0 || argumentSpec[0] === 'any') { - return; - } const entryCount = argumentSpec.length; let hasMoreTypesToCheck = argumentSpec.length > 1; for (const [index, type] of argumentSpec.entries()) { @@ -198,57 +196,76 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { }); } if (type.includes('-')) { - const arrayItemsType = type.slice(6); - let actualType: string | undefined; - for (const element of arg) { - try { - typeCheckArgument(element, [arrayItemsType]); - actualType = arrayItemsType; - } catch (error) { - if (!hasMoreTypesToCheck || actualType !== undefined) { - throw error; - } - } - } + checkComplexArrayType(arg, type, hasMoreTypesToCheck); } break; } if (type === 'expression') { - if (!(arg instanceof Expression)) { - if (!hasMoreTypesToCheck) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - } + checkExpressionType(arg, argumentSpec, hasMoreTypesToCheck); break; - } else if (type === 'string' || type === 'number' || type === 'boolean') { - if (typeof arg !== type) { - if (!hasMoreTypesToCheck) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - continue; + } else if (['string', 'number', 'boolean'].includes(type)) { + if (typeof arg !== type && !hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); } - break; - } else if (type === 'object') { - if (!isRecord(arg)) { - if (index === entryCount - 1) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } + if (typeof arg === type) { + break; } + } else if (type === 'object') { + checkObjectType(arg, argumentSpec, hasMoreTypesToCheck); break; } } }; +const checkComplexArrayType = ( + arg: unknown[], + type: string, + hasMoreTypesToCheck: boolean +): void => { + const arrayItemsType = type.slice(6); + let actualType: string | undefined; + for (const element of arg) { + try { + typeCheckArgument(element, [arrayItemsType]); + actualType = arrayItemsType; + } catch (error) { + if (!hasMoreTypesToCheck || actualType !== undefined) { + throw error; + } + } + } +}; + +const checkExpressionType = ( + arg: unknown, + type: string[], + hasMoreTypesToCheck: boolean +): void => { + if (!(arg instanceof Expression) && !hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: type, + actualType: getType(arg), + }); + } +}; + +const checkObjectType = ( + arg: unknown, + type: string[], + hasMoreTypesToCheck: boolean +): void => { + if (!isRecord(arg) && !hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: type, + actualType: getType(arg), + }); + } +}; + export { isTruthy, arityCheck, sliceArray, typeCheck, typeCheckArgument }; From d5060d660370929a2ccb32f14db0ff9036503674 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 18:23:53 +0100 Subject: [PATCH 096/103] chore: reduce complexity --- packages/jmespath/src/utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts index e787652bb3..ed95310bae 100644 --- a/packages/jmespath/src/utils.ts +++ b/packages/jmespath/src/utils.ts @@ -204,7 +204,8 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { checkExpressionType(arg, argumentSpec, hasMoreTypesToCheck); break; } else if (['string', 'number', 'boolean'].includes(type)) { - if (typeof arg !== type && !hasMoreTypesToCheck) { + const should = typeof arg !== type && !hasMoreTypesToCheck; + if (should) { throw new JMESPathTypeError({ currentValue: arg, expectedTypes: argumentSpec, From b108e2824f18ffe54b2f9f903367e2f4d5850816 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 18:28:59 +0100 Subject: [PATCH 097/103] chore: reduce complexity --- packages/jmespath/src/utils.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts index ed95310bae..cad22f25ef 100644 --- a/packages/jmespath/src/utils.ts +++ b/packages/jmespath/src/utils.ts @@ -204,14 +204,7 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { checkExpressionType(arg, argumentSpec, hasMoreTypesToCheck); break; } else if (['string', 'number', 'boolean'].includes(type)) { - const should = typeof arg !== type && !hasMoreTypesToCheck; - if (should) { - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } + typeCheckType(arg, type, argumentSpec, hasMoreTypesToCheck); if (typeof arg === type) { break; } @@ -222,6 +215,21 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { } }; +const typeCheckType = ( + arg: unknown, + type: string, + argumentSpec: string[], + hasMoreTypesToCheck: boolean +): void => { + if (typeof arg !== type && !hasMoreTypesToCheck) { + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); + } +}; + const checkComplexArrayType = ( arg: unknown[], type: string, From 3d83aefaabc7a99853bb5a0d2cd38253328824cd Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 18:35:24 +0100 Subject: [PATCH 098/103] chore: reduce complexity --- packages/jmespath/src/utils.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts index cad22f25ef..22f1bd6bae 100644 --- a/packages/jmespath/src/utils.ts +++ b/packages/jmespath/src/utils.ts @@ -195,9 +195,7 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { actualType: getType(arg), }); } - if (type.includes('-')) { - checkComplexArrayType(arg, type, hasMoreTypesToCheck); - } + checkComplexArrayType(arg, type, hasMoreTypesToCheck); break; } if (type === 'expression') { @@ -235,6 +233,7 @@ const checkComplexArrayType = ( type: string, hasMoreTypesToCheck: boolean ): void => { + if (!type.includes('-')) return; const arrayItemsType = type.slice(6); let actualType: string | undefined; for (const element of arg) { From 7760b00dd3b13181f5ad54619ebe3da61880cbcf Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 19:03:22 +0100 Subject: [PATCH 099/103] reduce complexity --- packages/jmespath/src/utils.ts | 49 ++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts index 22f1bd6bae..f78b876f50 100644 --- a/packages/jmespath/src/utils.ts +++ b/packages/jmespath/src/utils.ts @@ -180,8 +180,14 @@ const typeCheck = ( * @param argumentSpec */ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { - const entryCount = argumentSpec.length; - let hasMoreTypesToCheck = argumentSpec.length > 1; + /* const entryCount = argumentSpec.length; + let hasMoreTypesToCheck = argumentSpec.length > 1; */ + let broken = false; + argumentSpec.forEach((type, index) => { + if (broken) return; + broken = check(arg, type, index, argumentSpec); + }); + /* for (const [index, type] of argumentSpec.entries()) { hasMoreTypesToCheck = index < entryCount - 1; if (type.startsWith('array')) { @@ -211,6 +217,45 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { break; } } + */ +}; + +const check = ( + arg: unknown, + type: string, + index: number, + argumentSpec: string[] +): boolean => { + const hasMoreTypesToCheck = index < argumentSpec.length - 1; + if (type.startsWith('array')) { + if (!Array.isArray(arg)) { + if (hasMoreTypesToCheck) { + return false; + } + throw new JMESPathTypeError({ + currentValue: arg, + expectedTypes: argumentSpec, + actualType: getType(arg), + }); + } + checkComplexArrayType(arg, type, hasMoreTypesToCheck); + + return true; + } + if (type === 'expression') { + checkExpressionType(arg, argumentSpec, hasMoreTypesToCheck); + + return true; + } else if (['string', 'number', 'boolean'].includes(type)) { + typeCheckType(arg, type, argumentSpec, hasMoreTypesToCheck); + if (typeof arg === type) return true; + } else if (type === 'object') { + checkObjectType(arg, argumentSpec, hasMoreTypesToCheck); + + return true; + } + + return false; }; const typeCheckType = ( From 86a36a49d770db8802de2e4366ce280386f93998 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 19:16:23 +0100 Subject: [PATCH 100/103] reduce complexity --- packages/jmespath/src/utils.ts | 132 +++++++++++++++++---------------- 1 file changed, 68 insertions(+), 64 deletions(-) diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts index f78b876f50..aeecbba514 100644 --- a/packages/jmespath/src/utils.ts +++ b/packages/jmespath/src/utils.ts @@ -147,6 +147,19 @@ const arityCheck = ( /** * Type checks the arguments passed to a function against the expected types. * + * Type checking at runtime involves checking the top level type, + * and in the case of arrays, potentially checking the types of + * the elements in the array. + * + * If the list of types includes 'any', then the type check is a + * no-op. + * + * If the list of types includes more than one type, then the + * argument is checked against each type in the list. If the + * argument matches any of the types, then the type check + * passes. If the argument does not match any of the types, then + * a JMESPathTypeError is thrown. + * * @param args The arguments passed to the function * @param argumentsSpecs The expected types for each argument */ @@ -163,13 +176,6 @@ const typeCheck = ( /** * Type checks an argument against a list of types. * - * Type checking at runtime involves checking the top level type, - * and in the case of arrays, potentially checking the types of - * the elements in the array. - * - * If the list of types includes 'any', then the type check is a - * no-op. - * * If the list of types includes more than one type, then the * argument is checked against each type in the list. If the * argument matches any of the types, then the type check @@ -180,44 +186,11 @@ const typeCheck = ( * @param argumentSpec */ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { - /* const entryCount = argumentSpec.length; - let hasMoreTypesToCheck = argumentSpec.length > 1; */ - let broken = false; + let valid = false; argumentSpec.forEach((type, index) => { - if (broken) return; - broken = check(arg, type, index, argumentSpec); + if (valid) return; + valid = check(arg, type, index, argumentSpec); }); - /* - for (const [index, type] of argumentSpec.entries()) { - hasMoreTypesToCheck = index < entryCount - 1; - if (type.startsWith('array')) { - if (!Array.isArray(arg)) { - if (hasMoreTypesToCheck) { - continue; - } - throw new JMESPathTypeError({ - currentValue: arg, - expectedTypes: argumentSpec, - actualType: getType(arg), - }); - } - checkComplexArrayType(arg, type, hasMoreTypesToCheck); - break; - } - if (type === 'expression') { - checkExpressionType(arg, argumentSpec, hasMoreTypesToCheck); - break; - } else if (['string', 'number', 'boolean'].includes(type)) { - typeCheckType(arg, type, argumentSpec, hasMoreTypesToCheck); - if (typeof arg === type) { - break; - } - } else if (type === 'object') { - checkObjectType(arg, argumentSpec, hasMoreTypesToCheck); - break; - } - } - */ }; const check = ( @@ -238,19 +211,37 @@ const check = ( actualType: getType(arg), }); } - checkComplexArrayType(arg, type, hasMoreTypesToCheck); + checkComplexArrayType(arg, type, !hasMoreTypesToCheck); return true; } if (type === 'expression') { - checkExpressionType(arg, argumentSpec, hasMoreTypesToCheck); + try { + checkExpressionType(arg, argumentSpec); + } catch (error) { + if (!hasMoreTypesToCheck) { + throw error; + } + } return true; } else if (['string', 'number', 'boolean'].includes(type)) { - typeCheckType(arg, type, argumentSpec, hasMoreTypesToCheck); + try { + typeCheckType(arg, type, argumentSpec); + } catch (error) { + if (!hasMoreTypesToCheck) { + throw error; + } + } if (typeof arg === type) return true; } else if (type === 'object') { - checkObjectType(arg, argumentSpec, hasMoreTypesToCheck); + try { + typeCheckObjectType(arg, argumentSpec); + } catch (error) { + if (!hasMoreTypesToCheck) { + throw error; + } + } return true; } @@ -261,10 +252,10 @@ const check = ( const typeCheckType = ( arg: unknown, type: string, - argumentSpec: string[], - hasMoreTypesToCheck: boolean + argumentSpec: string[] + // hasMoreTypesToCheck: boolean ): void => { - if (typeof arg !== type && !hasMoreTypesToCheck) { + if (typeof arg !== type) { throw new JMESPathTypeError({ currentValue: arg, expectedTypes: argumentSpec, @@ -273,10 +264,19 @@ const typeCheckType = ( } }; +/** + * Check if the argument is an array of complex types. + * + * For each element in the array, check if the type of the element matches the type of the array. + * + * @param arg The argument to check + * @param type The type to check against + * @param shouldThrow Whether there are more types to check + */ const checkComplexArrayType = ( arg: unknown[], type: string, - hasMoreTypesToCheck: boolean + shouldThrow: boolean ): void => { if (!type.includes('-')) return; const arrayItemsType = type.slice(6); @@ -286,19 +286,21 @@ const checkComplexArrayType = ( typeCheckArgument(element, [arrayItemsType]); actualType = arrayItemsType; } catch (error) { - if (!hasMoreTypesToCheck || actualType !== undefined) { + if (shouldThrow || actualType !== undefined) { throw error; } } } }; -const checkExpressionType = ( - arg: unknown, - type: string[], - hasMoreTypesToCheck: boolean -): void => { - if (!(arg instanceof Expression) && !hasMoreTypesToCheck) { +/** + * Check if the argument is an expression. + * + * @param arg The argument to check + * @param type The type to check against + */ +const checkExpressionType = (arg: unknown, type: string[]): void => { + if (!(arg instanceof Expression)) { throw new JMESPathTypeError({ currentValue: arg, expectedTypes: type, @@ -307,12 +309,14 @@ const checkExpressionType = ( } }; -const checkObjectType = ( - arg: unknown, - type: string[], - hasMoreTypesToCheck: boolean -): void => { - if (!isRecord(arg) && !hasMoreTypesToCheck) { +/** + * Check if the argument is an object. + * + * @param arg The argument to check + * @param type The type to check against + */ +const typeCheckObjectType = (arg: unknown, type: string[]): void => { + if (!isRecord(arg)) { throw new JMESPathTypeError({ currentValue: arg, expectedTypes: type, From f52ebd0a81a232a1b535076fc9417a6026f2484a Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 19:21:29 +0100 Subject: [PATCH 101/103] reduce complexity --- packages/jmespath/src/utils.ts | 66 +++++++++++++++++----------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts index aeecbba514..567ec0bac5 100644 --- a/packages/jmespath/src/utils.ts +++ b/packages/jmespath/src/utils.ts @@ -211,37 +211,19 @@ const check = ( actualType: getType(arg), }); } - checkComplexArrayType(arg, type, !hasMoreTypesToCheck); + checkComplexArrayType(arg, type, hasMoreTypesToCheck); return true; } if (type === 'expression') { - try { - checkExpressionType(arg, argumentSpec); - } catch (error) { - if (!hasMoreTypesToCheck) { - throw error; - } - } + checkExpressionType(arg, argumentSpec, hasMoreTypesToCheck); return true; } else if (['string', 'number', 'boolean'].includes(type)) { - try { - typeCheckType(arg, type, argumentSpec); - } catch (error) { - if (!hasMoreTypesToCheck) { - throw error; - } - } + typeCheckType(arg, type, argumentSpec, hasMoreTypesToCheck); if (typeof arg === type) return true; } else if (type === 'object') { - try { - typeCheckObjectType(arg, argumentSpec); - } catch (error) { - if (!hasMoreTypesToCheck) { - throw error; - } - } + checkObjectType(arg, argumentSpec, hasMoreTypesToCheck); return true; } @@ -249,13 +231,21 @@ const check = ( return false; }; +/** + * Check if the argument is of the expected type. + * + * @param arg The argument to check + * @param type The type to check against + * @param argumentSpec The list of types to check against + * @param hasMoreTypesToCheck Whether there are more types to check + */ const typeCheckType = ( arg: unknown, type: string, - argumentSpec: string[] - // hasMoreTypesToCheck: boolean + argumentSpec: string[], + hasMoreTypesToCheck: boolean ): void => { - if (typeof arg !== type) { + if (typeof arg !== type && !hasMoreTypesToCheck) { throw new JMESPathTypeError({ currentValue: arg, expectedTypes: argumentSpec, @@ -267,16 +257,14 @@ const typeCheckType = ( /** * Check if the argument is an array of complex types. * - * For each element in the array, check if the type of the element matches the type of the array. - * * @param arg The argument to check * @param type The type to check against - * @param shouldThrow Whether there are more types to check + * @param hasMoreTypesToCheck Whether there are more types to check */ const checkComplexArrayType = ( arg: unknown[], type: string, - shouldThrow: boolean + hasMoreTypesToCheck: boolean ): void => { if (!type.includes('-')) return; const arrayItemsType = type.slice(6); @@ -286,7 +274,7 @@ const checkComplexArrayType = ( typeCheckArgument(element, [arrayItemsType]); actualType = arrayItemsType; } catch (error) { - if (shouldThrow || actualType !== undefined) { + if (!hasMoreTypesToCheck || actualType !== undefined) { throw error; } } @@ -298,9 +286,14 @@ const checkComplexArrayType = ( * * @param arg The argument to check * @param type The type to check against + * @param hasMoreTypesToCheck Whether there are more types to check */ -const checkExpressionType = (arg: unknown, type: string[]): void => { - if (!(arg instanceof Expression)) { +const checkExpressionType = ( + arg: unknown, + type: string[], + hasMoreTypesToCheck: boolean +): void => { + if (!(arg instanceof Expression) && !hasMoreTypesToCheck) { throw new JMESPathTypeError({ currentValue: arg, expectedTypes: type, @@ -314,9 +307,14 @@ const checkExpressionType = (arg: unknown, type: string[]): void => { * * @param arg The argument to check * @param type The type to check against + * @param hasMoreTypesToCheck Whether there are more types to check */ -const typeCheckObjectType = (arg: unknown, type: string[]): void => { - if (!isRecord(arg)) { +const checkObjectType = ( + arg: unknown, + type: string[], + hasMoreTypesToCheck: boolean +): void => { + if (!isRecord(arg) && !hasMoreTypesToCheck) { throw new JMESPathTypeError({ currentValue: arg, expectedTypes: type, From 9678d6e6e74991803eb770c9c5694e37cac96709 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 19:42:34 +0100 Subject: [PATCH 102/103] docs: add missing jsdoc --- packages/jmespath/src/utils.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/jmespath/src/utils.ts b/packages/jmespath/src/utils.ts index 567ec0bac5..fe115f3098 100644 --- a/packages/jmespath/src/utils.ts +++ b/packages/jmespath/src/utils.ts @@ -189,11 +189,19 @@ const typeCheckArgument = (arg: unknown, argumentSpec: Array): void => { let valid = false; argumentSpec.forEach((type, index) => { if (valid) return; - valid = check(arg, type, index, argumentSpec); + valid = checkIfArgumentTypeIsValid(arg, type, index, argumentSpec); }); }; -const check = ( +/** + * Check if the argument is of the expected type. + * + * @param arg The argument to check + * @param type The expected type + * @param index The index of the type we are checking + * @param argumentSpec The list of types to check against + */ +const checkIfArgumentTypeIsValid = ( arg: unknown, type: string, index: number, From 300938e7c13917e8f67a77bcd9f967fc8d41a0ff Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 12 Mar 2024 19:48:56 +0100 Subject: [PATCH 103/103] chore: stabilize sort --- packages/jmespath/src/Functions.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/jmespath/src/Functions.ts b/packages/jmespath/src/Functions.ts index a511c49974..e16e0a4e8f 100644 --- a/packages/jmespath/src/Functions.ts +++ b/packages/jmespath/src/Functions.ts @@ -362,8 +362,19 @@ class Functions { @Functions.signature({ argumentsSpecs: [['array-number', 'array-string']], }) - public funcSort(arg: Array): Array { - return arg.sort(); + public funcSort(arg: Array | Array): Array { + return arg.sort((a: string | number, b: string | number): number => { + if (typeof a === 'string') { + // We can safely cast a and b to string here because the signature decorator + // already enforces that all elements are of the same type + return a.localeCompare(b as string); + } + + // We can safely cast a and b to number here because the signature decorator + // already enforces that all elements are of the same type, so if they're not strings + // then they must be numbers + return (a as number) - (b as number); + }); } /**