stop using lodash

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2025-04-17 11:44:49 +02:00
parent af853ef1bc
commit 913cf78690
6 changed files with 213 additions and 95 deletions
@@ -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:^",
@@ -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('{{--}}');
});
});
@@ -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('');
}
@@ -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__'"`,
);
});
});
@@ -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<TContext extends object = object>(
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<TContext>(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<TContext extends object = object>(
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;
};
}
-1
View File
@@ -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