From 913cf78690a99618cb455068ad24454fce70824b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 17 Apr 2025 11:44:49 +0200 Subject: [PATCH] stop using lodash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../package.json | 3 +- .../config.test.ts | 42 +------ .../config.ts | 54 +-------- .../src/util/createPatternResolver.test.ts | 94 +++++++++++++++ .../src/util/createPatternResolver.ts | 114 ++++++++++++++++++ yarn.lock | 1 - 6 files changed, 213 insertions(+), 95 deletions(-) create mode 100644 plugins/events-backend-module-google-pubsub/src/util/createPatternResolver.test.ts create mode 100644 plugins/events-backend-module-google-pubsub/src/util/createPatternResolver.ts diff --git a/plugins/events-backend-module-google-pubsub/package.json b/plugins/events-backend-module-google-pubsub/package.json index 574747874f..441c857841 100644 --- a/plugins/events-backend-module-google-pubsub/package.json +++ b/plugins/events-backend-module-google-pubsub/package.json @@ -40,8 +40,7 @@ "@backstage/plugin-events-node": "workspace:^", "@backstage/types": "workspace:^", "@google-cloud/pubsub": "^4.10.0", - "@opentelemetry/api": "^1.9.0", - "lodash": "^4.17.21" + "@opentelemetry/api": "^1.9.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/events-backend-module-google-pubsub/src/GooglePubSubConsumingEventPublisher/config.test.ts b/plugins/events-backend-module-google-pubsub/src/GooglePubSubConsumingEventPublisher/config.test.ts index ab690f2050..bfe3c20773 100644 --- a/plugins/events-backend-module-google-pubsub/src/GooglePubSubConsumingEventPublisher/config.test.ts +++ b/plugins/events-backend-module-google-pubsub/src/GooglePubSubConsumingEventPublisher/config.test.ts @@ -17,10 +17,7 @@ import { mockServices } from '@backstage/backend-test-utils'; import { JsonObject } from '@backstage/types'; import { Message } from '@google-cloud/pubsub'; -import { - createPatternResolver, - readSubscriptionTasksFromConfig, -} from './config'; +import { readSubscriptionTasksFromConfig } from './config'; function makeMessage( data: JsonObject, @@ -195,40 +192,3 @@ describe('readSubscriptionTasksFromConfig', () => { ); }); }); - -describe('createPatternResolver', () => { - it('resolves patterns as expected', () => { - expect(createPatternResolver('foo')({})).toEqual('foo'); - expect(createPatternResolver('{{a.b}}')({ a: { b: 'test' } })).toEqual( - 'test', - ); - expect(createPatternResolver('{{a.b}}')({ a: { b: '7' } })).toEqual('7'); - expect( - createPatternResolver('{{a.b-c}}')({ a: { 'b-c': 'test' } }), - ).toEqual('test'); - expect( - createPatternResolver("{{a['b-c']}}")({ a: { 'b-c': 'test' } }), - ).toEqual('test'); - expect( - createPatternResolver(' {{ a.b }} ')({ a: { b: ' test ' } }), - ).toEqual(' test '); - }); - - it('throws on bad / missing context values', () => { - expect(() => - createPatternResolver('{{a.b}}')({ a: { b: [7] } }), - ).toThrowErrorMatchingInlineSnapshot( - `"Expected string or number value for selector 'a.b', got object"`, - ); - expect(() => - createPatternResolver('{{a.b}}')({ a: {} }), - ).toThrowErrorMatchingInlineSnapshot(`"No value for selector 'a.b'"`); - }); - - it('just passes down broken parts of patterns', () => { - expect(createPatternResolver('-{{a}}-}}')({ a: 1 })).toEqual('-1-}}'); - expect(createPatternResolver('{{-{{a}}-')({ a: 1 })).toEqual('{{-1-'); - expect(createPatternResolver('{{-{{a}}-}}')({ a: 1 })).toEqual('{{-1-}}'); - expect(createPatternResolver('{{-{{}}-}}')({ a: 1 })).toEqual('{{--}}'); - }); -}); diff --git a/plugins/events-backend-module-google-pubsub/src/GooglePubSubConsumingEventPublisher/config.ts b/plugins/events-backend-module-google-pubsub/src/GooglePubSubConsumingEventPublisher/config.ts index b79b8feaeb..fdcab2e2e5 100644 --- a/plugins/events-backend-module-google-pubsub/src/GooglePubSubConsumingEventPublisher/config.ts +++ b/plugins/events-backend-module-google-pubsub/src/GooglePubSubConsumingEventPublisher/config.ts @@ -18,8 +18,8 @@ import { RootConfigService } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { Message } from '@google-cloud/pubsub'; +import { createPatternResolver } from '../util/createPatternResolver'; import { SubscriptionTask } from './types'; -import lodash from 'lodash'; export function readSubscriptionTasksFromConfig( rootConfig: RootConfigService, @@ -34,7 +34,7 @@ export function readSubscriptionTasksFromConfig( return subscriptionsConfig.keys().map(subscriptionId => { if (!subscriptionId.match(/^[-_\w]+$/)) { throw new InputError( - `Expected Googoe Pub/Sub subscription ID to consist of letters, numbers, dashes and lodashes, but got '${subscriptionId}'`, + `Expected Googoe Pub/Sub subscription ID to consist of letters, numbers, dashes and underscores, but got '${subscriptionId}'`, ); } @@ -112,7 +112,7 @@ function readMetadataMapper( metadata[key] = value; } } catch { - // ignore silently, keep original unchanged + // ignore silently, keep original } }); } @@ -128,51 +128,3 @@ function readMetadataMapper( return result; }; } - -/** - * Takes a pattern string that may contain `{{ path.to.value }}` placeholders, - * and returns a function that accepts a context object and returns strings that - * have had its placeholders filled in by using `lodash.get` accordingly on the - * context. - */ -export function createPatternResolver( - pattern: string, -): (context: unknown) => string { - // This split results in an array where even elements are static strings - // between placeholders, and odd elements are the contents inside - // placeholders. - // - // For example, the pattern: - // "{{ foo }}-{{bar}}{{baz}}." - // will result in: - // ['', 'foo', '-', 'bar', '', 'baz', .'] - const patternParts = pattern.split(/\{{\s*([\w\[\]'_.-]*)\s*}}/g); - - const resolvers = new Array<(context: unknown) => string>(); - - for (let i = 0; i < patternParts.length; i += 2) { - const staticPart = patternParts[i]; - const placeholderPart = patternParts[i + 1]; - - if (staticPart) { - resolvers.push(() => staticPart); - } - - if (placeholderPart) { - resolvers.push(context => { - const value = lodash.get(context, placeholderPart); - if (typeof value === 'string' || Number.isFinite(value)) { - return String(value); - } else if (!value) { - throw new InputError(`No value for selector '${placeholderPart}'`); - } else { - throw new InputError( - `Expected string or number value for selector '${placeholderPart}', got ${typeof value}`, - ); - } - }); - } - } - - return context => resolvers.map(resolver => resolver(context)).join(''); -} diff --git a/plugins/events-backend-module-google-pubsub/src/util/createPatternResolver.test.ts b/plugins/events-backend-module-google-pubsub/src/util/createPatternResolver.test.ts new file mode 100644 index 0000000000..555ec40476 --- /dev/null +++ b/plugins/events-backend-module-google-pubsub/src/util/createPatternResolver.test.ts @@ -0,0 +1,94 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createPatternResolver } from './createPatternResolver'; + +describe('createPatternResolver', () => { + it('resolves patterns as expected', () => { + expect(createPatternResolver('foo')({})).toEqual('foo'); + expect(createPatternResolver('{{a.b}}')({ a: { b: 'test' } })).toEqual( + 'test', + ); + expect(createPatternResolver('{{a.b}}')({ a: { b: '7' } })).toEqual('7'); + expect( + createPatternResolver('{{a.b-c}}')({ a: { 'b-c': 'test' } }), + ).toEqual('test'); + expect( + createPatternResolver("{{a['b-c']}}")({ a: { 'b-c': 'test' } }), + ).toEqual('test'); + expect( + createPatternResolver('{{a["b-c"]}}')({ a: { 'b-c': 'test' } }), + ).toEqual('test'); + expect( + createPatternResolver(' {{ a.b }} ')({ a: { b: ' test ' } }), + ).toEqual(' test '); + expect( + createPatternResolver('-{{ a.b[1] }}-')({ + a: { b: ['first', 'second'] }, + }), + ).toEqual('-second-'); + expect( + createPatternResolver('-{{ a.b.0 }}-')({ + a: { b: ['first', 'second'] }, + }), + ).toEqual('-first-'); + }); + + it('throws on bad / missing context values', () => { + expect(() => + createPatternResolver('{{a.b}}')({ a: { b: [7] } }), + ).toThrowErrorMatchingInlineSnapshot( + `"Expected string or number value for selector 'a.b', got object"`, + ); + expect(() => + createPatternResolver('{{a.b}}')({ a: {} }), + ).toThrowErrorMatchingInlineSnapshot(`"No value for selector 'a.b'"`); + expect(() => + createPatternResolver("-{{ a.b['length'] }}-")({ + a: { b: ['first', 'second'] }, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"No value for selector 'a.b['length']'"`, + ); + expect(() => + createPatternResolver('-{{ a.b.length }}-')({ + a: { b: ['first', 'second'] }, + }), + ).toThrowErrorMatchingInlineSnapshot( + `"No value for selector 'a.b.length'"`, + ); + }); + + it('just passes down broken parts of patterns', () => { + expect(createPatternResolver('-{{a}}-}}')({ a: 1 })).toEqual('-1-}}'); + expect(createPatternResolver('{{-{{a}}-')({ a: 1 })).toEqual('{{-1-'); + expect(createPatternResolver('{{-{{a}}-}}')({ a: 1 })).toEqual('{{-1-}}'); + expect(createPatternResolver('{{-{{}}-}}')({ a: 1 })).toEqual('{{--}}'); + }); + + it('only follows own properties', () => { + expect(() => + createPatternResolver('{{a.constructor}}')({ a: { b: 'test' } }), + ).toThrowErrorMatchingInlineSnapshot( + `"No value for selector 'a.constructor'"`, + ); + expect(() => + createPatternResolver('{{a.__proto__}}')({ a: { b: 'test' } }), + ).toThrowErrorMatchingInlineSnapshot( + `"No value for selector 'a.__proto__'"`, + ); + }); +}); diff --git a/plugins/events-backend-module-google-pubsub/src/util/createPatternResolver.ts b/plugins/events-backend-module-google-pubsub/src/util/createPatternResolver.ts new file mode 100644 index 0000000000..01e0b74e2e --- /dev/null +++ b/plugins/events-backend-module-google-pubsub/src/util/createPatternResolver.ts @@ -0,0 +1,114 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; + +/** + * Takes a pattern string that may contain `{{ path.to.value }}` placeholders, + * and returns a function that accepts a context object and returns strings that + * have had its placeholders filled in by following the dot separated path of + * properties accordingly on the context. + */ +export function createPatternResolver( + pattern: string, +): (context: TContext) => string { + // This split results in an array where even elements are static strings + // between placeholders, and odd elements are the contents inside + // placeholders. + // + // For example, the pattern: + // "{{ foo }}-{{bar}}{{baz}}." + // will result in: + // ['', 'foo', '-', 'bar', '', 'baz', '.'] + const patternParts = pattern.split(/{{\s*([\w\[\]'"_.-]*)\s*}}/g); + + const resolvers = new Array<(context: TContext) => string>(); + + for (let i = 0; i < patternParts.length; i += 2) { + const staticPart = patternParts[i]; + const placeholderPart = patternParts[i + 1]; + + if (staticPart) { + resolvers.push(() => staticPart); + } + + if (placeholderPart) { + const getter = createGetter(placeholderPart); + resolvers.push(context => { + const value = getter(context); + if (typeof value === 'string' || Number.isFinite(value)) { + return String(value); + } else if (!value) { + throw new InputError(`No value for selector '${placeholderPart}'`); + } else { + throw new InputError( + `Expected string or number value for selector '${placeholderPart}', got ${typeof value}`, + ); + } + }); + } + } + + return context => resolvers.map(resolver => resolver(context)).join(''); +} + +function createGetter( + path: string, +): (context: TContext) => unknown | undefined { + // The resulti of the split contains quads: + // + // - any "regular" part + // - pure digits that were within brackets, if applicable + // - contents of a single quoted string that was within brackets, if applicable + // - contents of a double quoted string that was within brackets, if applicable + // + // For example, the path: + // foo.bar[0].baz["qux.e"]a + // will result in: + // [ + // 'foo', undefined, undefined, undefined, + // 'bar', '0', undefined, undefined, + // 'baz', undefined, 'qux.e', undefined, + // 'a' + // ] + // and then the empty elements are stripped away + const parts = path + .split(/\.|\[(?:(\d+)|'([^']+)'|"([^"]+)")\]\.?/g) + .filter(Boolean); + + return (context: TContext): unknown | undefined => { + let current = context; + for (const part of parts) { + if (typeof current !== 'object' || !current) { + return undefined; + } + + if (Array.isArray(current)) { + if (!part.match(/^\d+$/)) { + return undefined; + } + current = (current as any[])[Number(part)]; + } else { + if (!Object.hasOwn(current, part)) { + return undefined; + } + current = (current as any)[part]; + } + } + + return current; + }; +} diff --git a/yarn.lock b/yarn.lock index 340c3fe16e..bd16a0b412 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6399,7 +6399,6 @@ __metadata: "@backstage/types": "workspace:^" "@google-cloud/pubsub": "npm:^4.10.0" "@opentelemetry/api": "npm:^1.9.0" - lodash: "npm:^4.17.21" wait-for-expect: "npm:^3.0.2" languageName: unknown linkType: soft