feat(freben): add handling and docs for entity references

This commit is contained in:
Fredrik Adelöw
2020-09-21 13:33:36 +02:00
parent 01b511a2a8
commit 0222279021
10 changed files with 718 additions and 4 deletions
@@ -7,7 +7,7 @@ description: Architecture Decision Record (ADR) log on Default Catalog File Name
## Background
While the spec for the catalog file format is well described in
[ADR002](./adr002-default-catalog-file-format.md), guidance was note provided as
[ADR002](./adr002-default-catalog-file-format.md), guidance was not provided as
to the name of the catalog file.
Following discussion in
@@ -23,4 +23,4 @@ catalog-info.yaml
```
This name is a default, **not a requirement**. The catalog file will work with
Backstage irregardless of its name.
Backstage regardless of its name.
@@ -0,0 +1,69 @@
---
id: adrs-adr009
title: ADR009: Entity References
description: Architecture Decision Record (ADR) log on Entity References
---
## Background
While the spec for the catalog file format is well described in
[ADR002](./adr002-default-catalog-file-format.md), guidance was not provided as
to how one is expected to express references to other entities in the catalog.
There was also some confusion on how to reference entities in URLs in the
Backstage frontend.
Following discussion in
[Issue 1947](https://github.com/spotify/backstage/issues/1947), a decision was
made.
## Entity References in YAML files
The textual format, as written by humans, to reference entities by name is on
the following form, where square brackets denote optionality:
```
[<kind>:][<namespace>/]<name>
```
That is, it is composed of between one and three parts in this specific order,
without any additional encoding, with those exact separator characters.
Optionality of `kind` and `namespace` are contextual, and they may or may not
have default contextual fallback values.
When that format is insufficient or when machine made interchange formats wish
to express such relations in a more expressive form, a nested structure on the
following form can be used:
```yaml
kind: <kind>
namespace: <namespace>
name: <name>
```
Of these, only `name` is always required. Optionality of `kind` and `namespace`
are contextual, and they may or may not have default contextual fallback values.
All other possible key values in this structure are reserved for future use.
A system or user wanting to express a full entity name that is always valid,
shall supply the entire triplet whether using the string form or the compound
form.
A full description of the format can be found
[in the documentation](https://backstage.io/docs/features/software-catalog/references).
## Entity References in URLs
Where entities are referenced by name in the Backstage frontend, the URL
containing the reference shall take the following form:
```
:namespace/:kind/:name
```
All three parts are required under all circumstances. The default value for the
`namespace` in the catalog is the string `"default"`, if the entity does not
specify one explicitly in `metadata.namespace`.
This means that we do not encourage the string form of entity references to be
used as a single URL segment, due to the use of URL-unsafe characters leading to
possible risk, confusion, and uglier URLs.
@@ -0,0 +1,104 @@
---
id: references
title: Entity References
description: How to express references between entities
---
Entities commonly have a need to reference other entities. For example, a
[Component](descriptor-format.md#kind-component) entity may want to declare who
its owner is by mentioning a Group or User entity, and a User entity may want to
declare what Group entities it is a member of. This article describes how to
write those references in your yaml entity declaration files.
Each entity in the catalog is uniquely identified by the triplet of its
[kind](descriptor-format.md#apiversion-and-kind-required),
[namespace](descriptor-format.md#namespace-optional), and
[name](descriptor-format.md#name-required). But that's a lot to type out
manually, and in a lot of circumstances, both the kind and the namespace are
fixed, or possible to deduce, or could have sane default values. So in order to
help the writer, the catalog has a few tricks up its sleeve.
Each reference can be expressed in one of two ways: as a compact string, or as a
compound reference structure.
## String References
This is the most common alternative, that should be used in almost all
circumstances.
The string is on the form `[<kind>:][<namespace>/]<name>`, that is, it is
composed of between one and three parts in this specific order, without any
additional encoding:
- Optionally, the kind, followed by a colon
- Optionally, the namespace, followed by a forward slash
- The name
The name is always required. Depending on the context, you may be able to leave
out the kind and/or namespace. If you do, it is contextual what values will be
used, and the relevant documentation should specify which rule applies where.
All strings are case insensitive.
```yaml
# Example:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: petstore
namespace: external-systems
description: Petstore
spec:
type: service
lifecycle: experimental
owner: group:pet-managers
implementsApis:
- petstore
- internal/streetlights
- hello-world
```
The field `spec.owner` is a reference. In this case, the string
`group:pet-managers` was given by the user. That means that the kind is `Group`,
the namespace is left out, and the name is `pet-managers`. In this context, the
namespace was chosen to fall back to the value `default` by the code that parsed
the reference, so the end result is that we expect to find another entity in the
catalog that is of kind `Group`, namespace `default` (which, actually, also can
be left out in its own yaml file because that's the default value there too),
and name `pet-managers`.
The entries in `implementsApis` are also references. In this case, none of them
need to specify a kind since we know from the context that that's the only kind
that's supported here. The second entry specifies a namespace but the other ones
don't, and in this context, the default is to refer to the same namespace as the
originating entity (`external-systems` here). So the three references
essentially expand to `api:external-systems/petstore`,
`api:internal/streetlights`, and `api:external-systems/hello-world`. We expect
there to exist three API kind entities in the catalog matching those references.
## Compound References
This is a more verbose version of a reference, where each part of the
kind-namespace-name triplet is expressed as a field in a structure. This format
can be used where necessary, such as if either of the three elements contain
colons or forward slashes. Avoid using it where possible, since it is harder to
read and write for humans.
```yaml
# Example:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: petstore
description: Petstore
spec:
type: service
lifecycle: experimental
owner:
kind: Group
name: aegis-imports/pet-managers
```
In this example, the `spec.owner` has been broken apart since the name was
complex. The kind happened to be written with an uppercase letter G, which also
works. The namespace was left out just like in the string version above, which
is handled identically.
+3 -1
View File
@@ -43,6 +43,7 @@
"features/software-catalog/configuration",
"features/software-catalog/system-model",
"features/software-catalog/descriptor-format",
"features/software-catalog/references",
"features/software-catalog/well-known-annotations",
"features/software-catalog/extending-the-model",
"features/software-catalog/external-integrations",
@@ -158,7 +159,8 @@
"architecture-decisions/adrs-adr005",
"architecture-decisions/adrs-adr006",
"architecture-decisions/adrs-adr007",
"architecture-decisions/adrs-adr008"
"architecture-decisions/adrs-adr008",
"architecture-decisions/adrs-adr009"
],
"Contribute": ["../CONTRIBUTING"],
"Support": ["overview/support"],
+2
View File
@@ -32,6 +32,7 @@ nav:
- Configuration: 'features/software-catalog/configuration.md'
- System model: 'features/software-catalog/system-model.md'
- YAML File Format: 'features/software-catalog/descriptor-format.md'
- Entity References: 'features/software-catalog/references.md'
- Well-known Annotations: 'features/software-catalog/well-known-annotations.md'
- Extending the model: 'features/software-catalog/extending-the-model.md'
- External integrations: 'features/software-catalog/external-integrations.md'
@@ -104,6 +105,7 @@ nav:
- ADR006 - Avoid React.FC and React.SFC: 'architecture-decisions/adr006-avoid-react-fc.md'
- ADR007 - Use MSW for Network Request Mocking: 'architecture-decisions/adr007-use-msw-to-mock-service-requests.md'
- ADR008 - Default Catalog File Name: 'architecture-decisions/adr008-default-catalog-file-name.md'
- ADR009 - Entity References: 'architecture-decisions/adr009-entity-references.md'
- Contribute: '../CONTRIBUTING.md'
- Support: 'overview/support.md'
- FAQ: FAQ.md
@@ -17,6 +17,8 @@
export { entityMetaGeneratedFields } from './Entity';
export type { Entity, EntityMeta } from './Entity';
export * from './policies';
export { parseEntityName, parseEntityRef, serializeEntityRef } from './ref';
export type { EntityRefContext } from './ref';
export {
entityHasChanges,
generateEntityEtag,
@@ -0,0 +1,355 @@
/*
* Copyright 2020 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 { parseEntityName, parseEntityRef, serializeEntityRef } from './ref';
describe('ref', () => {
describe('parseEntityName', () => {
it('handles some omissions', () => {
expect(parseEntityName('a:b/c')).toEqual({
kind: 'a',
namespace: 'b',
name: 'c',
});
expect(() => parseEntityName('b/c')).toThrow(/kind/);
expect(parseEntityName('a:c')).toEqual({
kind: 'a',
namespace: 'default',
name: 'c',
});
expect(() => parseEntityName('c')).toThrow(/kind/);
});
it('rejects bad inputs', () => {
expect(() => parseEntityName(null as any)).toThrow();
expect(() => parseEntityName(7 as any)).toThrow();
expect(() => parseEntityName('a:b:c')).toThrow();
expect(() => parseEntityName('a/b/c')).toThrow();
expect(() => parseEntityName('a/b:c')).toThrow();
expect(() => parseEntityName('a:b/c/d')).toThrow();
expect(() => parseEntityName('a:b/c:d')).toThrow();
});
it('rejects empty parts in strings', () => {
// one is empty
expect(() => parseEntityName(':b/c')).toThrow();
expect(() => parseEntityName('a:/c')).toThrow();
expect(() => parseEntityName('a:b/')).toThrow();
// two are empty
expect(() => parseEntityName('a:/')).toThrow();
expect(() => parseEntityName(':b/')).toThrow();
expect(() => parseEntityName(':/c')).toThrow();
// three are empty
expect(() => parseEntityName(':/')).toThrow();
// one is left out, one empty
expect(() => parseEntityName('/c')).toThrow();
expect(() => parseEntityName('b/')).toThrow();
expect(() => parseEntityName(':c')).toThrow();
expect(() => parseEntityName('a:')).toThrow();
// nothing at all
expect(() => parseEntityName('')).toThrow();
});
it('rejects empty parts in compounds', () => {
// one is empty
expect(() =>
parseEntityName({ kind: '', namespace: 'b', name: 'c' }),
).toThrow();
expect(() =>
parseEntityName({ kind: 'a', namespace: '', name: 'c' }),
).toThrow();
expect(() =>
parseEntityName({ kind: 'a', namespace: 'b', name: '' }),
).toThrow();
// two are empty
expect(() =>
parseEntityName({ kind: '', namespace: '', name: 'c' }),
).toThrow();
expect(() =>
parseEntityName({ kind: '', namespace: 'b', name: '' }),
).toThrow();
expect(() =>
parseEntityName({ kind: 'a', namespace: '', name: '' }),
).toThrow();
// three are empty
expect(() =>
parseEntityName({ kind: '', namespace: '', name: '' }),
).toThrow();
// one is left out, one empty
expect(() => parseEntityName({ namespace: '', name: 'c' })).toThrow();
expect(() => parseEntityName({ namespace: 'b', name: '' })).toThrow();
expect(() => parseEntityName({ kind: '', name: 'c' })).toThrow();
expect(() => parseEntityName({ kind: 'a', name: '' })).toThrow();
// nothing at all
expect(() => parseEntityName({} as any)).toThrow();
});
it('adds defaults where necessary to strings', () => {
expect(
parseEntityName('a:b/c', { defaultKind: 'x', defaultNamespace: 'y' }),
).toEqual({ kind: 'a', namespace: 'b', name: 'c' });
expect(
parseEntityName('b/c', { defaultKind: 'x', defaultNamespace: 'y' }),
).toEqual({ kind: 'x', namespace: 'b', name: 'c' });
expect(
parseEntityName('a:c', { defaultKind: 'x', defaultNamespace: 'y' }),
).toEqual({ kind: 'a', namespace: 'y', name: 'c' });
expect(parseEntityName('a:c', { defaultKind: 'x' })).toEqual({
kind: 'a',
namespace: 'default',
name: 'c',
});
expect(
parseEntityName('c', { defaultKind: 'x', defaultNamespace: 'y' }),
).toEqual({ kind: 'x', namespace: 'y', name: 'c' });
expect(parseEntityName('c', { defaultKind: 'x' })).toEqual({
kind: 'x',
namespace: 'default',
name: 'c',
});
});
it('adds defaults where necessary to compounds', () => {
expect(
parseEntityName(
{ kind: 'a', namespace: 'b', name: 'c' },
{ defaultKind: 'x', defaultNamespace: 'y' },
),
).toEqual({ kind: 'a', namespace: 'b', name: 'c' });
expect(
parseEntityName(
{ namespace: 'b', name: 'c' },
{ defaultKind: 'x', defaultNamespace: 'y' },
),
).toEqual({ kind: 'x', namespace: 'b', name: 'c' });
expect(
parseEntityName(
{ kind: 'a', name: 'c' },
{ defaultKind: 'x', defaultNamespace: 'y' },
),
).toEqual({ kind: 'a', namespace: 'y', name: 'c' });
expect(
parseEntityName({ kind: 'a', name: 'c' }, { defaultKind: 'x' }),
).toEqual({ kind: 'a', namespace: 'default', name: 'c' });
expect(
parseEntityName(
{ name: 'c' },
{ defaultKind: 'x', defaultNamespace: 'y' },
),
).toEqual({ kind: 'x', namespace: 'y', name: 'c' });
expect(parseEntityName({ name: 'c' }, { defaultKind: 'x' })).toEqual({
kind: 'x',
namespace: 'default',
name: 'c',
});
// empty strings are errors, not defaults
expect(() =>
parseEntityName(
{ kind: '', namespace: 'b', name: 'c' },
{ defaultKind: 'x', defaultNamespace: 'y' },
),
).toThrow(/kind/);
expect(() =>
parseEntityName(
{ kind: 'a', namespace: '', name: 'c' },
{ defaultKind: 'x', defaultNamespace: 'y' },
),
).toThrow(/namespace/);
});
});
describe('parseEntityRef', () => {
it('handles some omissions', () => {
expect(parseEntityRef('a:b/c')).toEqual({
kind: 'a',
namespace: 'b',
name: 'c',
});
expect(parseEntityRef('b/c')).toEqual({
kind: undefined,
namespace: 'b',
name: 'c',
});
expect(parseEntityRef('a:c')).toEqual({
kind: 'a',
namespace: undefined,
name: 'c',
});
expect(parseEntityRef('c')).toEqual({
kind: undefined,
namespace: undefined,
name: 'c',
});
});
it('rejects bad inputs', () => {
expect(() => parseEntityRef(null as any)).toThrow();
expect(() => parseEntityRef(7 as any)).toThrow();
expect(() => parseEntityRef('a:b:c')).toThrow();
expect(() => parseEntityRef('a/b/c')).toThrow();
expect(() => parseEntityRef('a/b:c')).toThrow();
expect(() => parseEntityRef('a:b/c/d')).toThrow();
expect(() => parseEntityRef('a:b/c:d')).toThrow();
});
it('rejects empty parts in strings', () => {
// one is empty
expect(() => parseEntityRef(':b/c')).toThrow();
expect(() => parseEntityRef('a:/c')).toThrow();
expect(() => parseEntityRef('a:b/')).toThrow();
// two are empty
expect(() => parseEntityRef('a:/')).toThrow();
expect(() => parseEntityRef(':b/')).toThrow();
expect(() => parseEntityRef(':/c')).toThrow();
// three are empty
expect(() => parseEntityRef(':/')).toThrow();
// one is left out, one empty
expect(() => parseEntityRef('/c')).toThrow();
expect(() => parseEntityRef('b/')).toThrow();
expect(() => parseEntityRef(':c')).toThrow();
expect(() => parseEntityRef('a:')).toThrow();
// nothing at all
expect(() => parseEntityRef('')).toThrow();
});
it('rejects empty parts in compounds', () => {
// one is empty
expect(() =>
parseEntityRef({ kind: '', namespace: 'b', name: 'c' }),
).toThrow();
expect(() =>
parseEntityRef({ kind: 'a', namespace: '', name: 'c' }),
).toThrow();
expect(() =>
parseEntityRef({ kind: 'a', namespace: 'b', name: '' }),
).toThrow();
// two are empty
expect(() =>
parseEntityRef({ kind: '', namespace: '', name: 'c' }),
).toThrow();
expect(() =>
parseEntityRef({ kind: '', namespace: 'b', name: '' }),
).toThrow();
expect(() =>
parseEntityRef({ kind: 'a', namespace: '', name: '' }),
).toThrow();
// three are empty
expect(() =>
parseEntityRef({ kind: '', namespace: '', name: '' }),
).toThrow();
// one is left out, one empty
expect(() => parseEntityRef({ namespace: '', name: 'c' })).toThrow();
expect(() => parseEntityRef({ namespace: 'b', name: '' })).toThrow();
expect(() => parseEntityRef({ kind: '', name: 'c' })).toThrow();
expect(() => parseEntityRef({ kind: 'a', name: '' })).toThrow();
// nothing at all
expect(() => parseEntityRef({} as any)).toThrow();
});
it('adds defaults where necessary to strings', () => {
expect(
parseEntityRef('a:b/c', { defaultKind: 'x', defaultNamespace: 'y' }),
).toEqual({ kind: 'a', namespace: 'b', name: 'c' });
expect(
parseEntityRef('b/c', { defaultKind: 'x', defaultNamespace: 'y' }),
).toEqual({ kind: 'x', namespace: 'b', name: 'c' });
expect(
parseEntityRef('a:c', { defaultKind: 'x', defaultNamespace: 'y' }),
).toEqual({ kind: 'a', namespace: 'y', name: 'c' });
expect(
parseEntityRef('c', { defaultKind: 'x', defaultNamespace: 'y' }),
).toEqual({ kind: 'x', namespace: 'y', name: 'c' });
});
it('adds defaults where necessary to compounds', () => {
expect(
parseEntityRef(
{ kind: 'a', namespace: 'b', name: 'c' },
{ defaultKind: 'x', defaultNamespace: 'y' },
),
).toEqual({ kind: 'a', namespace: 'b', name: 'c' });
expect(
parseEntityRef(
{ namespace: 'b', name: 'c' },
{ defaultKind: 'x', defaultNamespace: 'y' },
),
).toEqual({ kind: 'x', namespace: 'b', name: 'c' });
expect(
parseEntityRef(
{ kind: 'a', name: 'c' },
{ defaultKind: 'x', defaultNamespace: 'y' },
),
).toEqual({ kind: 'a', namespace: 'y', name: 'c' });
expect(
parseEntityRef(
{ name: 'c' },
{ defaultKind: 'x', defaultNamespace: 'y' },
),
).toEqual({ kind: 'x', namespace: 'y', name: 'c' });
// empty strings are errors, not defaults
expect(() =>
parseEntityRef(
{ kind: '', namespace: 'b', name: 'c' },
{ defaultKind: 'x', defaultNamespace: 'y' },
),
).toThrow(/kind/);
expect(() =>
parseEntityRef(
{ kind: 'a', namespace: '', name: 'c' },
{ defaultKind: 'x', defaultNamespace: 'y' },
),
).toThrow(/namespace/);
});
});
describe('serializeEntityRef', () => {
it('handles partials', () => {
expect(
serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c' }),
).toEqual('a:b/c');
expect(serializeEntityRef({ namespace: 'b', name: 'c' })).toEqual('b/c');
expect(serializeEntityRef({ kind: 'a', name: 'c' })).toEqual('a:c');
expect(serializeEntityRef({ name: 'c' })).toEqual('c');
});
it('picks the least complex form', () => {
expect(
serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c' }),
).toEqual('a:b/c');
expect(serializeEntityRef({ namespace: 'b', name: 'c' })).toEqual('b/c');
expect(serializeEntityRef({ kind: 'a', name: 'c' })).toEqual('a:c');
expect(serializeEntityRef({ name: 'c' })).toEqual('c');
expect(
serializeEntityRef({ kind: 'a:x', namespace: 'b', name: 'c' }),
).toEqual({ kind: 'a:x', namespace: 'b', name: 'c' });
expect(
serializeEntityRef({ kind: 'a/x', namespace: 'b', name: 'c' }),
).toEqual({ kind: 'a/x', namespace: 'b', name: 'c' });
expect(
serializeEntityRef({ kind: 'a', namespace: 'b:x', name: 'c' }),
).toEqual({ kind: 'a', namespace: 'b:x', name: 'c' });
expect(
serializeEntityRef({ kind: 'a', namespace: 'b/x', name: 'c' }),
).toEqual({ kind: 'a', namespace: 'b/x', name: 'c' });
expect(
serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c:x' }),
).toEqual({ kind: 'a', namespace: 'b', name: 'c:x' });
expect(
serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c/x' }),
).toEqual({ kind: 'a', namespace: 'b', name: 'c/x' });
});
});
});
+146
View File
@@ -0,0 +1,146 @@
/*
* Copyright 2020 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 { CompoundEntityRef, EntityName, EntityRef } from '../types';
/**
* The context of defaults that entity reference parsing happens within.
*/
export type EntityRefContext = {
/** The default kind, if none is given in the reference */
defaultKind?: string;
/** The default namespace, if none is given in the reference */
defaultNamespace?: string;
};
/**
* Parses an entity reference, either on string or compound form, and always
* returns a complete entity name including kind, namespace and name.
*
* This function automatically assumes the default namespace "default" unless
* otherwise specified as part of the options, and will throw an error if no
* kind was specified in the input reference and no default kind was given.
*
* @param ref The reference to parse
* @param context The context of defaults that the parsing happens within
* @returns A complete entity name
*/
export function parseEntityName(
ref: EntityRef,
context: EntityRefContext = {},
): EntityName {
const { kind, namespace, name } = parseEntityRef(ref, {
defaultNamespace: 'default',
...context,
});
if (!kind) {
throw new Error(
`Entity reference ${namespace}/${name} did not contain a kind`,
);
}
// This is a corner case - when the ref contained a namespace but it was the
// empty string (so the default did not kick in)
if (!namespace) {
throw new Error(
`Entity reference ${kind}:${name} did not contain a namespace`,
);
}
return {
kind,
namespace: namespace!,
name,
};
}
/**
* Parses an entity reference, either on string or compound form, and returns
* a structure with a name, and optional kind and namespace.
*
* The options object can contain default values for the kind and namespace,
* that will be used if the input reference did not specify any.
*
* @param ref The reference to parse
* @param context The context of defaults that the parsing happens within
* @returns The compound form of the reference
*/
export function parseEntityRef(
ref: EntityRef,
context: EntityRefContext = {},
): CompoundEntityRef {
if (!ref) {
throw new Error(`Entity reference must not be empty`);
}
if (typeof ref === 'string') {
const match = /^(?:([^:/]+):)?(?:([^:/]+)\/)?([^:/]+)$/.exec(ref.trim());
if (!match) {
throw new Error(
`Entity reference "${ref}" was not on the form [<kind>:][<namespace>/]<name>`,
);
}
return {
kind: match[1] ?? context.defaultKind,
namespace: match[2] ?? context.defaultNamespace,
name: match[3],
};
}
const { kind, namespace, name } = ref;
if (kind === '') {
throw new Error('Entity reference kinds must not be empty');
} else if (namespace === '') {
throw new Error('Entity reference namespaces must not be empty');
} else if (!name) {
throw new Error('Entity references must contain a name');
}
return {
kind: kind ?? context.defaultKind,
namespace: namespace ?? context.defaultNamespace,
name,
};
}
/**
* Takes an entity reference or name, and outputs an entity reference on the
* most compact form possible. I.e. if the parts do not contain any
* special/reserved characters, it outputs the string form, otherwise it
* outputs the compound form.
*
* @param ref The reference to serialize
* @returns The same reference on either string or compound form
*/
export function serializeEntityRef(
ref: CompoundEntityRef | EntityName,
): EntityRef {
const { kind, namespace, name } = ref;
if (
kind?.includes(':') ||
kind?.includes('/') ||
namespace?.includes(':') ||
namespace?.includes('/') ||
name.includes(':') ||
name.includes('/')
) {
return { kind, namespace, name };
}
return `${kind ? `${kind}:` : ''}${namespace ? `${namespace}/` : ''}${name}`;
}
+7 -1
View File
@@ -18,5 +18,11 @@ export * from './entity';
export { EntityPolicies } from './EntityPolicies';
export * from './kinds';
export * from './location';
export type { EntityPolicy, JSONSchema } from './types';
export type {
CompoundEntityRef,
EntityName,
EntityPolicy,
EntityRef,
JSONSchema,
} from './types';
export * from './validation';
+28
View File
@@ -34,3 +34,31 @@ export type EntityPolicy = {
};
export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue };
/**
* A complete entity name, with the full kind-namespace-name triplet.
*/
export type EntityName = {
kind: string;
namespace: string;
name: string;
};
/**
* A reference by name to an entity, where the kind and/or the namespace can be
* left out.
*
* Left-out parts of the reference need to be handled by the application,
* either by rejecting the reference or by falling back to default values.
*/
export type CompoundEntityRef = {
kind?: string;
namespace?: string;
name: string;
};
/**
* A reference by name to an entity, either as a compact string representation,
* or as a compound reference structure.
*/
export type EntityRef = string | CompoundEntityRef;