frontend-app-api: add support for all config shorthands

Co-authored-by: Fredrik Adelöw <freben@gmail.com>
Co-authored-by: Johan Haals <johan.haals@gmail.com>
Co-authored-by: Vincenzo Scamporlino <vincenzos@spotify.com>
Co-authored-by: Philipp Hugenroth <philipph@spotify.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-08-23 16:28:34 +02:00
parent 42839fd014
commit d1d555f0dc
2 changed files with 152 additions and 24 deletions
@@ -172,4 +172,88 @@ describe('readAppExtensionParameters', () => {
},
]);
});
it('should support extension implementation shorthand', () => {
expect(
readAppExtensionParameters(
new ConfigReader({
app: {
extensions: [{ 'core.router': 'example-package#CustomRouter' }],
},
}),
ref => ({ ref } as unknown as Extension<unknown>),
),
).toEqual([
{
id: 'core.router',
extension: { ref: 'example-package#CustomRouter' },
},
]);
});
it('should support attachment shorthand', () => {
expect(
readAppExtensionParameters(
new ConfigReader({
app: {
extensions: [
{
'core.router/routes': {
extension: 'example-package#MyPage',
config: { foo: 'bar' },
},
},
],
},
}),
ref => ({ ref } as unknown as Extension<unknown>),
),
).toEqual([
{
id: 'generated.1',
at: 'core.router/routes',
extension: { ref: 'example-package#MyPage' },
config: { foo: 'bar' },
},
]);
});
it('should support attachment with extension shorthand', () => {
expect(
readAppExtensionParameters(
new ConfigReader({
app: {
extensions: [{ 'core.router/routes': 'example-package#MyPage' }],
},
}),
ref => ({ ref } as unknown as Extension<unknown>),
),
).toEqual([
{
id: 'generated.1',
at: 'core.router/routes',
extension: { ref: 'example-package#MyPage' },
},
]);
});
it('should reject attachment shorthand with explicit attachment', () => {
expect(() =>
readAppExtensionParameters(
new ConfigReader({
app: {
extensions: [
{
'core.router/routes': {
at: 'other/input',
},
},
],
},
}),
),
).toThrow(
`Invalid extension configuration at app.extensions[0][core.router/routes], must not specify 'at' when using attachment shorthand form`,
);
});
});
@@ -15,7 +15,10 @@
*/
import { Config } from '@backstage/config';
import { ExtensionInstanceParameters } from '@backstage/frontend-plugin-api';
import {
Extension,
ExtensionInstanceParameters,
} from '@backstage/frontend-plugin-api';
// Since we'll never merge arrays in config the config reader context
// isn't too much of a help. Fall back to manual config reading logic
@@ -23,6 +26,9 @@ import { ExtensionInstanceParameters } from '@backstage/frontend-plugin-api';
/** @internal */
export function readAppExtensionParameters(
rootConfig: Config,
resolveExtensionRef: (ref: string) => Extension<unknown> = () => {
throw new Error('Not Implemented');
},
): Partial<ExtensionInstanceParameters>[] {
const arr = rootConfig.getOptional('app.extensions');
if (!Array.isArray(arr)) {
@@ -34,54 +40,92 @@ export function readAppExtensionParameters(
return [];
}
return arr.map((value, index) => {
const generateIndex = (() => {
let generatedIndex = 1;
return () => `generated.${generatedIndex++}`;
})();
return arr.map((arrayEntry, index) => {
function errorMsg(msg: string, key?: string, prop?: string) {
return `Invalid extension configuration at app.extensions[${index}]${
key ? `[${key}]` : ''
}${prop ? `.${prop}` : ''}, ${msg}`;
}
if (typeof value === 'string') {
return { id: value };
} else if (
typeof value !== 'object' ||
value === null ||
Array.isArray(value)
// Example YAML:
// - entity.card.about
if (typeof arrayEntry === 'string') {
return { id: arrayEntry };
}
// All other forms are single-key objects
if (
typeof arrayEntry !== 'object' ||
arrayEntry === null ||
Array.isArray(arrayEntry)
) {
throw new Error(errorMsg('must be a string or an object'));
}
const keys = Object.keys(value);
const keys = Object.keys(arrayEntry);
if (keys.length !== 1) {
const joinedKeys = `"${keys.join('", "')}"`;
throw new Error(errorMsg(`must have exactly one key, got ${joinedKeys}`));
}
const key = keys[0];
const obj = value[key];
if (typeof obj === 'boolean') {
return { id: key, disabled: !obj };
}
let value = arrayEntry[key];
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
let at = key.includes('/') ? key : undefined;
const id = at ? generateIndex() : key;
if (typeof value === 'boolean') {
if (at) {
throw new Error(
errorMsg('cannot be applied to an instance input', key),
);
}
value = { disabled: !value };
} else if (typeof value === 'string') {
value = { extension: value };
} else if (
typeof value !== 'object' ||
value === null ||
Array.isArray(value)
) {
throw new Error(errorMsg('must be an object', key));
}
const at = obj.at;
if (at !== undefined && typeof at !== 'string') {
const atRef = value.at;
if (atRef !== undefined && typeof atRef !== 'string') {
throw new Error(errorMsg('must be a string', key, 'at'));
} else if (atRef !== undefined) {
if (at) {
throw new Error(
errorMsg(
`must not specify 'at' when using attachment shorthand form`,
key,
),
);
}
at = atRef;
}
const disabled = obj.disabled;
const disabled = value.disabled;
if (disabled !== undefined && typeof disabled !== 'boolean') {
throw new Error(errorMsg('must be a boolean', key, 'disabled'));
}
const extension = obj.extension;
if (extension !== undefined && typeof extension !== 'string') {
const extensionRef = value.extension;
if (extensionRef !== undefined && typeof extensionRef !== 'string') {
throw new Error(errorMsg('must be a string', key, 'extension'));
}
if (extension) {
throw new Error('TODO: implement extension resolution');
}
return { id: key, at, disabled, config: obj.config /* validate later */ };
const extension = extensionRef
? resolveExtensionRef(extensionRef)
: undefined;
return {
id,
at,
disabled,
extension,
config: value.config /* validate later */,
};
});
}