1 | // Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved.
|
2 | // Node module: @loopback/context
|
3 | // This file is licensed under the MIT License.
|
4 | // License text available at https://opensource.org/licenses/MIT
|
5 |
|
6 | import {generateUniqueId} from './unique-id';
|
7 |
|
8 | export type BindingAddress<T = unknown> = string | BindingKey<T>;
|
9 |
|
10 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
11 | export class BindingKey<ValueType> {
|
12 | static readonly PROPERTY_SEPARATOR = '#';
|
13 |
|
14 | /**
|
15 | * Create a new key for a binding bound to a value of type `ValueType`.
|
16 | *
|
17 | * @example
|
18 | *
|
19 | * ```ts
|
20 | * BindingKey.create<string>('application.name');
|
21 | * BindingKey.create<number>('config', 'rest.port);
|
22 | * BindingKey.create<number>('config#rest.port');
|
23 | * ```
|
24 | *
|
25 | * @param key - The binding key. When propertyPath is not provided, the key
|
26 | * is allowed to contain propertyPath as encoded via `BindingKey#toString()`
|
27 | * @param propertyPath - Optional path to a deep property of the bound value.
|
28 | */
|
29 | public static create<V>(key: string, propertyPath?: string): BindingKey<V> {
|
30 | // TODO(bajtos) allow chaining of propertyPaths, e.g.
|
31 | // BindingKey.create('config#rest', 'port')
|
32 | // should create {key: 'config', path: 'rest.port'}
|
33 | if (propertyPath) {
|
34 | BindingKey.validate(key);
|
35 | return new BindingKey<V>(key, propertyPath);
|
36 | }
|
37 |
|
38 | return BindingKey.parseKeyWithPath(key);
|
39 | }
|
40 |
|
41 | private constructor(
|
42 | public readonly key: string,
|
43 | public readonly propertyPath?: string,
|
44 | ) {}
|
45 |
|
46 | toString() {
|
47 | return this.propertyPath
|
48 | ? `${this.key}${BindingKey.PROPERTY_SEPARATOR}${this.propertyPath}`
|
49 | : this.key;
|
50 | }
|
51 |
|
52 | /**
|
53 | * Get a binding address for retrieving a deep property of the object
|
54 | * bound to the current binding key.
|
55 | *
|
56 | * @param propertyPath - A dot-separated path to a (deep) property, e.g. "server.port".
|
57 | */
|
58 | deepProperty<PropertyValueType>(propertyPath: string) {
|
59 | // TODO(bajtos) allow chaining of propertyPaths, e.g.
|
60 | // BindingKey.create('config', 'rest').deepProperty('port')
|
61 | // should create {key: 'config', path: 'rest.port'}
|
62 | return BindingKey.create<PropertyValueType>(this.key, propertyPath);
|
63 | }
|
64 |
|
65 | /**
|
66 | * Validate the binding key format. Please note that `#` is reserved.
|
67 | * Returns a string representation of the binding key.
|
68 | *
|
69 | * @param key - Binding key, such as `a`, `a.b`, `a:b`, or `a/b`
|
70 | */
|
71 | static validate<T>(key: BindingAddress<T>): string {
|
72 | if (!key) throw new Error('Binding key must be provided.');
|
73 | key = key.toString();
|
74 | if (key.includes(BindingKey.PROPERTY_SEPARATOR)) {
|
75 | throw new Error(
|
76 | `Binding key ${key} cannot contain` +
|
77 | ` '${BindingKey.PROPERTY_SEPARATOR}'.`,
|
78 | );
|
79 | }
|
80 | return key;
|
81 | }
|
82 |
|
83 | /**
|
84 | * Parse a string containing both the binding key and the path to the deeply
|
85 | * nested property to retrieve.
|
86 | *
|
87 | * @param keyWithPath - The key with an optional path,
|
88 | * e.g. "application.instance" or "config#rest.port".
|
89 | */
|
90 | static parseKeyWithPath<T>(keyWithPath: BindingAddress<T>): BindingKey<T> {
|
91 | if (typeof keyWithPath !== 'string') {
|
92 | return BindingKey.create<T>(keyWithPath.key, keyWithPath.propertyPath);
|
93 | }
|
94 |
|
95 | const index = keyWithPath.indexOf(BindingKey.PROPERTY_SEPARATOR);
|
96 | if (index === -1) {
|
97 | return new BindingKey<T>(keyWithPath);
|
98 | }
|
99 |
|
100 | return BindingKey.create<T>(
|
101 | keyWithPath.slice(0, index).trim(),
|
102 | keyWithPath.slice(index + 1),
|
103 | );
|
104 | }
|
105 |
|
106 | /**
|
107 | * Name space for configuration binding keys
|
108 | */
|
109 | static CONFIG_NAMESPACE = '$config';
|
110 |
|
111 | /**
|
112 | * Build a binding key for the configuration of the given binding.
|
113 | * The format is `<key>:$config`
|
114 | *
|
115 | * @param key - Key of the target binding to be configured
|
116 | */
|
117 | static buildKeyForConfig<T>(key: BindingAddress = ''): BindingAddress<T> {
|
118 | const suffix = BindingKey.CONFIG_NAMESPACE;
|
119 | const bindingKey = key ? `${key}:${suffix}` : suffix;
|
120 | return bindingKey;
|
121 | }
|
122 |
|
123 | /**
|
124 | * Generate a universally unique binding key.
|
125 | *
|
126 | * Please note the format of they generated key is not specified, you must
|
127 | * not rely on any specific formatting (e.g. UUID style).
|
128 | *
|
129 | * @param namespace - Namespace for the binding
|
130 | */
|
131 | static generate<T>(namespace = ''): BindingKey<T> {
|
132 | const prefix = namespace ? `${namespace}.` : '';
|
133 | const name = generateUniqueId();
|
134 | return BindingKey.create(`${prefix}${name}`);
|
135 | }
|
136 | }
|