-
Notifications
You must be signed in to change notification settings - Fork 1
/
uri.ts
196 lines (185 loc) · 5.22 KB
/
uri.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
191
192
193
194
195
196
type Stringify =
| string
| number
| boolean;
type ParamValue =
| Stringify
| unknown[]
| string[][]
| Record<string, Stringify>
| URLSearchParams
| undefined
| null;
/**
* Removes the first and/or last character of a string if the character is a `/`.
*
* Returns the input given if it is not a string, otherwise it trims the string.
*
* _returning the input given instead of only allowing strings to be inputted through typescript is used for performance gains_
*/
export const trimSlashes = <T extends ParamValue>(input: T) => {
if (typeof input !== "string") return input;
const stripFirst = input[0] === "/";
const stripLast = input[input.length - 1] === "/";
if (stripLast || stripFirst) {
// typescript doesn't allow implicit conversion here
// `bool ^ 0` is about the fastest way to convert currently
// although `+bool` could be used for clarity in the future
// `Number(bool)` is very slow and should be avoided.
return input.slice(
(stripFirst as unknown as number) ^ 0,
input.length - (stripLast as unknown as number) ^ 0,
);
} else {
return input;
}
};
/**
* @private
* Encodes an object into a query string
*/
export const encodeURLQueryString = (params: Record<string, Stringify>) =>
Object.keys(params)
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
.join("&");
// TODO: test nested if performance compared to this
/** @private */
export const convertValue = (value: ParamValue) => {
if (value == null) return "";
if (Array.isArray(value)) return value.join("/");
if (typeof value === "object") {
if (value instanceof URLSearchParams) {
return value;
} else {
return encodeURLQueryString(value);
}
} else {
return encodeURIComponent(value);
}
};
/**
* Joins together values in a way that makes writing urls easier.
*
* # Usage
*
* ## starting string
* At the beginning of the template, if a string is inserted with nothing before it,
* the string is inserted without any encoding applied to it.
*
* If a foward slash `/` is the first or last character in the starting string, it will be removed..
* ```js
* # import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts"
* # import { uri } from './uri.ts'
*
* const HOST = `https://example.com/`
* assertEquals(
* uri`${HOST}/example`,
* 'https://example.com/example'
* )
* ```
*
* ## ending `?` cleanup
* if the template string ends with a `?`
* then the ? will be removed if there is no content after it.
*
* ```js
* # import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts"
* # import { uri } from './uri.ts'
*
* // removed when nothing after
* assertEquals(
* uri`https://example.com/example?${null}`,
* 'https://example.com/example'
* )
*
* // kept when something after
* assertEquals(
* uri`https://example.com/example?${{}}`,
* 'https://example.com/example?'
* )
*
* // avoiding `?` cleanup behavior always
* assertEquals(
* uri`https://example.com/example${['?']}${null}`,
* 'https://example.com/example?'
* )
* ```
*
* ## strings, numbers, and booleans
* strings are transformed using `encodeURIComponent`
* ```js
* # import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts"
* # import { uri } from './uri.ts'
*
* assertEquals(
* uri`https://example.com/${'hello world?'}`,
* 'https://example.com/hello%20world%3F'
* )
* ```
*
* ## objects
* objects get transformed into query strings.
* ```js
* # import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts"
* # import { uri } from './uri.ts'
*
* assertEquals(
* uri`https://example.com/${{ foo: 'bar', key: (10).toString() }}`,
* 'https://example.com/foo=bar&key=10'
* )
* ```
*
* ## arrays
* arrays are joined with `/` with no other special transformations applied.
* ```js
* # import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts"
* # import { uri } from './uri.ts'
*
* assertEquals(
* uri`https://example.com/${['get', 'user', 120]}`,
* 'https://example.com/get/user/120'
* )
* ```
*
* this can be used to avoid having your values transformed or escaped when inserting them.
* ```js
* # import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts"
* # import { uri } from './uri.ts'
*
* // before
* assertEquals(
* uri`https://example.com/${'>///<'}`,
* 'https://example.com/%3E%2F%2F%2F%3C'
* )
*
* // after
* assertEquals(
* uri`https://example.com/${['>///<']}`,
* 'https://example.com/>///<'
* )
* ```
*/
export const uri = (
strings: TemplateStringsArray,
...keys: ParamValue[]
) => {
let output = "";
for (let i = 0; i < strings.length; i++) {
let string = strings[i];
const insert = keys[i];
// if it's the final insert and it's an ?
// without anything after, skip inserting them
if (string.endsWith("?") && insert == null) {
string = string.slice(0, string.length - 1);
}
// if it's the first insert with nothing before it
// then insert it as a BASE
if (i === 0 && string === "") {
output += trimSlashes(insert);
} else {
output += `${string}${convertValue(insert)}`;
}
}
return output;
};
export default uri;