add parseLocationReference/stringifyLocationReference

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2021-03-05 13:56:25 +01:00
parent 98387ac6b5
commit 0b42fff222
15 changed files with 259 additions and 93 deletions
+3 -2
View File
@@ -19,15 +19,16 @@ import {
EntityName,
Location,
LOCATION_ANNOTATION,
stringifyLocationReference,
} from '@backstage/catalog-model';
import fetch from 'cross-fetch';
import {
AddLocationRequest,
AddLocationResponse,
CatalogRequestOptions,
CatalogApi,
CatalogEntitiesRequest,
CatalogListResponse,
CatalogRequestOptions,
DiscoveryApi,
} from './types';
@@ -135,7 +136,7 @@ export class CatalogClient implements CatalogApi {
);
return all
.map(r => r.data)
.find(l => locationCompound === `${l.type}:${l.target}`);
.find(l => locationCompound === stringifyLocationReference(l));
}
async removeEntityByUid(
@@ -0,0 +1,70 @@
/*
* Copyright 2021 Spotify AB
*
* 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 { parseLocationReference, stringifyLocationReference } from './helpers';
describe('parseLocationReference', () => {
it('works for the simple case', () => {
expect(parseLocationReference('url:https://www.google.com')).toEqual({
type: 'url',
target: 'https://www.google.com',
});
});
it('rejects faulty inputs', () => {
expect(() => parseLocationReference(7 as any)).toThrow(
"Unable to parse location reference '7', unexpected argument number",
);
expect(() => parseLocationReference('')).toThrow(
"Unable to parse location reference '', expected '<type>:<target>', e.g. 'url:https://host/path'",
);
expect(() => parseLocationReference('hello')).toThrow(
"Unable to parse location reference 'hello', expected '<type>:<target>', e.g. 'url:https://host/path'",
);
expect(() => parseLocationReference(':hello')).toThrow(
"Unable to parse location reference ':hello', expected '<type>:<target>', e.g. 'url:https://host/path'",
);
expect(() => parseLocationReference('hello:')).toThrow(
"Unable to parse location reference 'hello:', expected '<type>:<target>', e.g. 'url:https://host/path'",
);
expect(() => parseLocationReference('http://blah')).toThrow(
"Invalid location reference 'http://blah', please prefix it with 'url:', e.g. 'url:http://blah'",
);
expect(() => parseLocationReference('https://bleh')).toThrow(
"Invalid location reference 'https://bleh', please prefix it with 'url:', e.g. 'url:https://bleh'",
);
});
});
describe('stringifyLocationReference', () => {
it('works for the simple case', () => {
expect(
stringifyLocationReference({
type: 'url',
target: 'https://www.google.com',
}),
).toEqual('url:https://www.google.com');
});
it('rejects faulty inputs', () => {
expect(() =>
stringifyLocationReference({ type: '', target: 'hello' }),
).toThrow('Unable to stringify location reference, empty type');
expect(() =>
stringifyLocationReference({ type: 'hello', target: '' }),
).toThrow('Unable to stringify location reference, empty target');
});
});
@@ -0,0 +1,82 @@
/*
* Copyright 2021 Spotify AB
*
* 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.
*/
/**
* Parses a string form location reference.
*
* Note that the return type is not `LocationSpec`, because we do not want to
* conflate the string form with the additional properties of that type.
*
* @param ref A string-form location reference, e.g. 'url:https://host'
* @returns A location reference, e.g. { type: 'url', target: 'https://host' }
*/
export function parseLocationReference(
ref: string,
): { type: string; target: string } {
if (typeof ref !== 'string') {
throw new TypeError(
`Unable to parse location reference '${ref}', unexpected argument ${typeof ref}`,
);
}
const splitIndex = ref.indexOf(':');
if (splitIndex < 0) {
throw new TypeError(
`Unable to parse location reference '${ref}', expected '<type>:<target>', e.g. 'url:https://host/path'`,
);
}
const type = ref.substr(0, splitIndex).trim();
const target = ref.substr(splitIndex + 1).trim();
if (!type || !target) {
throw new TypeError(
`Unable to parse location reference '${ref}', expected '<type>:<target>', e.g. 'url:https://host/path'`,
);
}
if (type === 'http' || type === 'https') {
throw new TypeError(
`Invalid location reference '${ref}', please prefix it with 'url:', e.g. 'url:${ref}'`,
);
}
return { type, target };
}
/**
* Turns a location reference into its string form.
*
* Note that the input type is not `LocationSpec`, because we do not want to
* conflate the string form with the additional properties of that type.
*
* @param ref A location reference, e.g. { type: 'url', target: 'https://host' }
* @returns A string-form location reference, e.g. 'url:https://host'
*/
export function stringifyLocationReference(ref: {
type: string;
target: string;
}): string {
const { type, target } = ref;
if (!type) {
throw new TypeError(`Unable to stringify location reference, empty type`);
} else if (!target) {
throw new TypeError(`Unable to stringify location reference, empty target`);
}
return `${type}:${target}`;
}
+7 -6
View File
@@ -14,14 +14,15 @@
* limitations under the License.
*/
export type { Location, LocationSpec } from './types';
export {
locationSchema,
locationSpecSchema,
analyzeLocationSchema,
} from './validation';
export {
LOCATION_ANNOTATION,
ORIGIN_LOCATION_ANNOTATION,
SOURCE_LOCATION_ANNOTATION,
} from './annotation';
export { parseLocationReference, stringifyLocationReference } from './helpers';
export type { Location, LocationSpec } from './types';
export {
analyzeLocationSchema,
locationSchema,
locationSpecSchema,
} from './validation';
+1 -1
View File
@@ -105,7 +105,7 @@ describe('parseReferenceAnnotation', () => {
'backstage.io/techdocs-ref',
mockEntityWithBadAnnotation,
);
}).toThrow(/Failure to parse/);
}).toThrow(/Unable to parse/);
});
});
+6 -18
View File
@@ -15,7 +15,7 @@
*/
import { Git, InputError, UrlReader } from '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import { Entity, parseLocationReference } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import fs from 'fs-extra';
import parseGitUrl from 'git-url-parse';
@@ -36,28 +36,15 @@ export const parseReferenceAnnotation = (
entity: Entity,
): ParsedLocationAnnotation => {
const annotation = entity.metadata.annotations?.[annotationName];
if (!annotation) {
throw new InputError(
`No location annotation provided in entity: ${entity.metadata.name}`,
);
}
// split on the first colon for the protocol and the rest after the first split
// is the location.
const [type, target] = annotation.split(/:(.+)/) as [
RemoteProtocol?,
string?,
];
if (!type || !target) {
throw new InputError(
`Failure to parse either protocol or location for entity: ${entity.metadata.name}`,
);
}
const { type, target } = parseLocationReference(annotation);
return {
type,
type: type as RemoteProtocol,
target,
};
};
@@ -77,8 +64,9 @@ export const getLocationForEntity = (
case 'url':
return { type, target };
case 'dir':
if (path.isAbsolute(target)) return { type, target };
if (path.isAbsolute(target)) {
return { type, target };
}
return parseReferenceAnnotation(
'backstage.io/managed-by-location',
entity,