From 7d81ee14a2aaf08a089e5a409d39ef830325ad76 Mon Sep 17 00:00:00 2001 From: Rhys Arkins Date: Mon, 7 Oct 2024 10:37:16 +0200 Subject: [PATCH] feat(packageRules): matchJsonata Fixes #31820 Add `matchJsonata` configuration option to `packageRules`. * Add new option `matchJsonata` of type string with parent `packageRules` in `lib/config/options/index.ts`. * Create `JsonataMatcher` class in `lib/util/package-rules/jsonata.ts` extending `Matcher` and implement `matches` method using JSONata. * Import `JsonataMatcher` in `lib/util/package-rules/matchers.ts` and add it to the `matchers` array. * Add tests for `JsonataMatcher` class in `lib/util/package-rules/jsonata.spec.ts`. * Update types to include `matchJsonata` under `packageRules` in `lib/config/types.ts`. --- lib/config/options/index.ts | 11 ++++ lib/config/types.ts | 1 + lib/util/package-rules/jsonata.spec.ts | 69 ++++++++++++++++++++++++++ lib/util/package-rules/jsonata.ts | 22 ++++++++ lib/util/package-rules/matchers.ts | 2 + 5 files changed, 105 insertions(+) create mode 100644 lib/util/package-rules/jsonata.spec.ts create mode 100644 lib/util/package-rules/jsonata.ts diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 95b3b68a623568..73b3eff57e172c 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -1513,6 +1513,17 @@ const options: RenovateOptions[] = [ cli: false, env: false, }, + { + name: 'matchJsonata', + description: + 'A JSONata expression to match against the full config object. Valid only within a `packageRules` object.', + type: 'string', + stage: 'package', + parents: ['packageRules'], + mergeable: true, + cli: false, + env: false, + }, // Version behavior { name: 'allowedVersions', diff --git a/lib/config/types.ts b/lib/config/types.ts index d92dc7886c5bfd..38155f307c02ae 100644 --- a/lib/config/types.ts +++ b/lib/config/types.ts @@ -381,6 +381,7 @@ export interface PackageRule matchRepositories?: string[]; matchSourceUrls?: string[]; matchUpdateTypes?: UpdateType[]; + matchJsonata?: string; registryUrls?: string[] | null; vulnerabilitySeverity?: string; vulnerabilityFixVersion?: string; diff --git a/lib/util/package-rules/jsonata.spec.ts b/lib/util/package-rules/jsonata.spec.ts new file mode 100644 index 00000000000000..f23f24b6258cdd --- /dev/null +++ b/lib/util/package-rules/jsonata.spec.ts @@ -0,0 +1,69 @@ +import { JsonataMatcher } from './jsonata'; + +describe('JsonataMatcher', () => { + const matcher = new JsonataMatcher(); + + it('should return true for a matching JSONata expression', () => { + const result = matcher.matches( + { depName: 'lodash' }, + { matchJsonata: '$.depName = "lodash"' } + ); + expect(result).toBeTrue(); + }); + + it('should return false for a non-matching JSONata expression', () => { + const result = matcher.matches( + { depName: 'lodash' }, + { matchJsonata: '$.depName = "react"' } + ); + expect(result).toBeFalse(); + }); + + it('should return false for an invalid JSONata expression', () => { + const result = matcher.matches( + { depName: 'lodash' }, + { matchJsonata: '$.depName = ' } + ); + expect(result).toBeFalse(); + }); + + it('should return null if matchJsonata is not defined', () => { + const result = matcher.matches( + { depName: 'lodash' }, + {} + ); + expect(result).toBeNull(); + }); + + it('should return true for a complex JSONata expression', () => { + const result = matcher.matches( + { depName: 'lodash', version: '4.17.21' }, + { matchJsonata: '$.depName = "lodash" and $.version = "4.17.21"' } + ); + expect(result).toBeTrue(); + }); + + it('should return false for a complex JSONata expression with non-matching version', () => { + const result = matcher.matches( + { depName: 'lodash', version: '4.17.20' }, + { matchJsonata: '$.depName = "lodash" and $.version = "4.17.21"' } + ); + expect(result).toBeFalse(); + }); + + it('should return true for a JSONata expression with nested properties', () => { + const result = matcher.matches( + { dep: { name: 'lodash', version: '4.17.21' } }, + { matchJsonata: '$.dep.name = "lodash" and $.dep.version = "4.17.21"' } + ); + expect(result).toBeTrue(); + }); + + it('should return false for a JSONata expression with nested properties and non-matching version', () => { + const result = matcher.matches( + { dep: { name: 'lodash', version: '4.17.20' } }, + { matchJsonata: '$.dep.name = "lodash" and $.dep.version = "4.17.21"' } + ); + expect(result).toBeFalse(); + }); +}); diff --git a/lib/util/package-rules/jsonata.ts b/lib/util/package-rules/jsonata.ts new file mode 100644 index 00000000000000..36ef3bffc0cb9d --- /dev/null +++ b/lib/util/package-rules/jsonata.ts @@ -0,0 +1,22 @@ +import jsonata from 'jsonata'; +import type { PackageRule, PackageRuleInputConfig } from '../../config/types'; +import { Matcher } from './base'; + +export class JsonataMatcher extends Matcher { + override matches( + inputConfig: PackageRuleInputConfig, + { matchJsonata }: PackageRule, + ): boolean | null { + if (!matchJsonata) { + return null; + } + + try { + const expression = jsonata(matchJsonata); + const result = expression.evaluate(inputConfig); + return Boolean(result); + } catch (error) { + return false; + } + } +} diff --git a/lib/util/package-rules/matchers.ts b/lib/util/package-rules/matchers.ts index 9ad89afe0a92aa..8f988a80a45908 100644 --- a/lib/util/package-rules/matchers.ts +++ b/lib/util/package-rules/matchers.ts @@ -13,6 +13,7 @@ import { NewValueMatcher } from './new-value'; import { PackageNameMatcher } from './package-names'; import { RepositoriesMatcher } from './repositories'; import { SourceUrlsMatcher } from './sourceurls'; +import { JsonataMatcher } from './jsonata'; import type { MatcherApi } from './types'; import { UpdateTypesMatcher } from './update-types'; @@ -40,3 +41,4 @@ matchers.push(new UpdateTypesMatcher()); matchers.push(new SourceUrlsMatcher()); matchers.push(new NewValueMatcher()); matchers.push(new CurrentAgeMatcher()); +matchers.push(new JsonataMatcher());