forked from deeplay-io/nice-grpc
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Metadata.ts
190 lines (157 loc) · 4.91 KB
/
Metadata.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
/**
* Metadata is information about a particular RPC call (such as authentication
* details) in the form of a list of key-value pairs, where the keys are strings
* and the values are strings or binary data.
*/
export type Metadata = {
/**
* Sets the value of a metadata with a given key.
*
* The value must be binary if and only if the key ends with '-bin'.
*
* Multiple string metadata values are always joined to a single string with
* a comma.
*/
set<Key extends string>(
key: Key,
value: MetadataValue<Key> | Array<MetadataValue<Key>>,
): Metadata;
/**
* Appends the value to an array of metadata with a given key.
*
* The value must be binary if and only if the key ends with '-bin'.
*
* Multiple string metadata values are always joined to a single string with
* a comma.
*/
append<Key extends string>(key: Key, value: MetadataValue<Key>): Metadata;
/**
* Clears all values of a metadata with a given key.
*/
delete(key: string): void;
/**
* Returns the value of a metadata with a given key.
*
* If there are multiple binary values, the first one is returned.
*
* Multiple string metadata values are always joined to a single string with
* a comma.
*/
get<Key extends string>(key: Key): MetadataValue<Key> | undefined;
/**
* Returns an array of all the values of a metadata with a given key.
*
* Multiple string metadata values are always joined to a single string with
* a comma.
*/
getAll<Key extends string>(key: Key): Array<MetadataValue<Key>>;
/**
* Checks whether there is at least one value of a metadata with a given key.
*/
has(key: string): boolean;
[Symbol.iterator](): IterableIterator<[string, Array<string | Uint8Array>]>;
};
export type MetadataValue<Key extends string> = string extends Key
? string | Uint8Array
: Lowercase<Key> extends `${string}-bin`
? Uint8Array
: string;
export type MetadataInit =
| Metadata
| Iterable<[string, string | Uint8Array | Array<string | Uint8Array>]>
| Record<string, string | Uint8Array | Array<string | Uint8Array>>;
export type MetadataConstructor = {
new (init?: MetadataInit): Metadata;
(init?: MetadataInit): Metadata;
};
export const Metadata = function Metadata(init?: MetadataInit): Metadata {
const data = new Map<string, Array<string | Uint8Array>>();
const metadata = {
set(key: string, value: string | Uint8Array | Array<string | Uint8Array>) {
key = normalizeKey(key);
if (Array.isArray(value)) {
if (value.length === 0) {
data.delete(key);
} else {
for (const item of value) {
validate(key, item);
}
data.set(key, key.endsWith('-bin') ? value : [value.join(', ')]);
}
} else {
validate(key, value);
data.set(key, [value]);
}
return metadata;
},
append(key: string, value: string | Uint8Array) {
key = normalizeKey(key);
validate(key, value);
let values = data.get(key);
if (values == null) {
values = [];
data.set(key, values);
}
values.push(value);
if (!key.endsWith('-bin')) {
data.set(key, [values.join(', ')]);
}
return metadata;
},
delete(key: string) {
key = normalizeKey(key);
data.delete(key);
},
get<Key extends string>(key: string): MetadataValue<Key> | undefined {
key = normalizeKey(key);
return data.get(key)?.[0] as MetadataValue<Key> | undefined;
},
getAll<Key extends string>(key: string): Array<MetadataValue<Key>> {
key = normalizeKey(key);
return (data.get(key) ?? []) as Array<MetadataValue<Key>>;
},
has(key: string) {
key = normalizeKey(key);
return data.has(key);
},
[Symbol.iterator]() {
return data[Symbol.iterator]();
},
};
if (init != null) {
const entries = isIterable(init) ? init : Object.entries(init);
for (const [key, value] of entries) {
metadata.set(key, value);
}
}
return metadata;
} as MetadataConstructor;
function normalizeKey(key: string): string {
return key.toLowerCase();
}
function validate(key: string, value: string | Uint8Array): void {
if (!/^[0-9a-z_.-]+$/.test(key)) {
throw new Error(`Metadata key '${key}' contains illegal characters`);
}
if (key.endsWith('-bin')) {
if (!(value instanceof Uint8Array)) {
throw new Error(
`Metadata key '${key}' ends with '-bin', thus it must have binary value`,
);
}
} else {
if (typeof value !== 'string') {
throw new Error(
`Metadata key '${key}' doesn't end with '-bin', thus it must have string value`,
);
}
if (!/^[ -~]*$/.test(value)) {
throw new Error(
`Metadata value '${value}' of key '${key}' contains illegal characters`,
);
}
}
}
function isIterable(value: object): value is Iterable<unknown> {
return Symbol.iterator in value;
}