@grain/stdlib 0.5.13 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +201 -0
- package/LICENSE +1 -1
- package/README.md +25 -2
- package/array.gr +1512 -199
- package/array.md +2032 -94
- package/bigint.gr +239 -140
- package/bigint.md +450 -106
- package/buffer.gr +595 -102
- package/buffer.md +903 -145
- package/bytes.gr +401 -110
- package/bytes.md +551 -63
- package/char.gr +228 -49
- package/char.md +373 -7
- package/exception.gr +26 -12
- package/exception.md +29 -5
- package/float32.gr +130 -109
- package/float32.md +185 -57
- package/float64.gr +112 -99
- package/float64.md +185 -57
- package/hash.gr +62 -40
- package/hash.md +27 -3
- package/int16.gr +430 -0
- package/int16.md +618 -0
- package/int32.gr +200 -269
- package/int32.md +254 -289
- package/int64.gr +142 -225
- package/int64.md +254 -289
- package/int8.gr +511 -0
- package/int8.md +786 -0
- package/json.gr +2071 -0
- package/json.md +646 -0
- package/list.gr +120 -68
- package/list.md +125 -80
- package/map.gr +560 -57
- package/map.md +672 -56
- package/marshal.gr +239 -227
- package/marshal.md +36 -4
- package/number.gr +626 -676
- package/number.md +738 -153
- package/option.gr +33 -35
- package/option.md +58 -42
- package/package.json +2 -2
- package/path.gr +148 -187
- package/path.md +47 -96
- package/pervasives.gr +75 -416
- package/pervasives.md +85 -180
- package/priorityqueue.gr +433 -74
- package/priorityqueue.md +422 -54
- package/queue.gr +362 -80
- package/queue.md +433 -38
- package/random.gr +67 -75
- package/random.md +68 -40
- package/range.gr +135 -63
- package/range.md +198 -43
- package/rational.gr +284 -0
- package/rational.md +545 -0
- package/regex.gr +933 -1066
- package/regex.md +59 -60
- package/result.gr +23 -25
- package/result.md +54 -39
- package/runtime/atof/common.gr +78 -82
- package/runtime/atof/common.md +22 -10
- package/runtime/atof/decimal.gr +102 -127
- package/runtime/atof/decimal.md +28 -7
- package/runtime/atof/lemire.gr +56 -71
- package/runtime/atof/lemire.md +9 -1
- package/runtime/atof/parse.gr +83 -110
- package/runtime/atof/parse.md +12 -2
- package/runtime/atof/slow.gr +28 -35
- package/runtime/atof/slow.md +9 -1
- package/runtime/atof/table.gr +19 -18
- package/runtime/atof/table.md +10 -2
- package/runtime/atoi/parse.gr +153 -136
- package/runtime/atoi/parse.md +50 -1
- package/runtime/bigint.gr +410 -517
- package/runtime/bigint.md +71 -57
- package/runtime/compare.gr +176 -85
- package/runtime/compare.md +31 -1
- package/runtime/dataStructures.gr +144 -32
- package/runtime/dataStructures.md +267 -31
- package/runtime/debugPrint.gr +34 -15
- package/runtime/debugPrint.md +37 -5
- package/runtime/equal.gr +53 -52
- package/runtime/equal.md +30 -1
- package/runtime/exception.gr +38 -47
- package/runtime/exception.md +10 -8
- package/runtime/gc.gr +23 -152
- package/runtime/gc.md +13 -17
- package/runtime/malloc.gr +31 -31
- package/runtime/malloc.md +11 -3
- package/runtime/numberUtils.gr +193 -174
- package/runtime/numberUtils.md +29 -9
- package/runtime/numbers.gr +1695 -1021
- package/runtime/numbers.md +1098 -134
- package/runtime/string.gr +543 -245
- package/runtime/string.md +76 -6
- package/runtime/unsafe/constants.gr +30 -13
- package/runtime/unsafe/constants.md +80 -0
- package/runtime/unsafe/conv.gr +55 -28
- package/runtime/unsafe/conv.md +41 -9
- package/runtime/unsafe/memory.gr +10 -30
- package/runtime/unsafe/memory.md +15 -19
- package/runtime/unsafe/tags.gr +37 -21
- package/runtime/unsafe/tags.md +88 -8
- package/runtime/unsafe/wasmf32.gr +30 -36
- package/runtime/unsafe/wasmf32.md +64 -56
- package/runtime/unsafe/wasmf64.gr +30 -36
- package/runtime/unsafe/wasmf64.md +64 -56
- package/runtime/unsafe/wasmi32.gr +49 -66
- package/runtime/unsafe/wasmi32.md +102 -94
- package/runtime/unsafe/wasmi64.gr +52 -79
- package/runtime/unsafe/wasmi64.md +108 -100
- package/runtime/utils/printing.gr +13 -15
- package/runtime/utils/printing.md +11 -3
- package/runtime/wasi.gr +294 -295
- package/runtime/wasi.md +62 -42
- package/set.gr +574 -64
- package/set.md +634 -54
- package/stack.gr +181 -64
- package/stack.md +271 -42
- package/string.gr +453 -533
- package/string.md +241 -151
- package/uint16.gr +369 -0
- package/uint16.md +585 -0
- package/uint32.gr +470 -0
- package/uint32.md +737 -0
- package/uint64.gr +471 -0
- package/uint64.md +737 -0
- package/uint8.gr +369 -0
- package/uint8.md +585 -0
- package/uri.gr +1093 -0
- package/uri.md +477 -0
- package/{sys → wasi}/file.gr +914 -500
- package/{sys → wasi}/file.md +454 -50
- package/wasi/process.gr +292 -0
- package/{sys → wasi}/process.md +164 -6
- package/wasi/random.gr +77 -0
- package/wasi/random.md +80 -0
- package/{sys → wasi}/time.gr +15 -22
- package/{sys → wasi}/time.md +5 -5
- package/immutablearray.gr +0 -929
- package/immutablearray.md +0 -1038
- package/immutablemap.gr +0 -493
- package/immutablemap.md +0 -479
- package/immutablepriorityqueue.gr +0 -360
- package/immutablepriorityqueue.md +0 -291
- package/immutableset.gr +0 -498
- package/immutableset.md +0 -449
- package/runtime/debug.gr +0 -2
- package/runtime/debug.md +0 -6
- package/runtime/unsafe/errors.gr +0 -36
- package/runtime/unsafe/errors.md +0 -204
- package/sys/process.gr +0 -254
- package/sys/random.gr +0 -79
- package/sys/random.md +0 -66
package/uri.gr
ADDED
|
@@ -0,0 +1,1093 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for working with URIs.
|
|
3
|
+
*
|
|
4
|
+
* @example from "uri" include Uri
|
|
5
|
+
*
|
|
6
|
+
* @since v0.6.0
|
|
7
|
+
*/
|
|
8
|
+
module Uri
|
|
9
|
+
|
|
10
|
+
from "array" include Array
|
|
11
|
+
from "buffer" include Buffer
|
|
12
|
+
from "bytes" include Bytes
|
|
13
|
+
from "char" include Char
|
|
14
|
+
from "list" include List
|
|
15
|
+
from "number" include Number
|
|
16
|
+
from "option" include Option
|
|
17
|
+
from "result" include Result
|
|
18
|
+
from "string" include String
|
|
19
|
+
from "uint8" include Uint8
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Represents a parsed RFC 3986 URI.
|
|
23
|
+
*/
|
|
24
|
+
provide record Uri {
|
|
25
|
+
scheme: Option<String>,
|
|
26
|
+
userinfo: Option<String>,
|
|
27
|
+
host: Option<String>,
|
|
28
|
+
port: Option<Number>,
|
|
29
|
+
path: String,
|
|
30
|
+
query: Option<String>,
|
|
31
|
+
fragment: Option<String>,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Represents an error encountered while parsing a URI.
|
|
36
|
+
*/
|
|
37
|
+
provide enum ParseError {
|
|
38
|
+
ParseError,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Represents an error encountered while constructing a URI with `make` or `update`.
|
|
43
|
+
*/
|
|
44
|
+
provide enum ConstructUriError {
|
|
45
|
+
UserinfoWithNoHost,
|
|
46
|
+
PortWithNoHost,
|
|
47
|
+
InvalidSchemeError,
|
|
48
|
+
InvalidUserinfoError,
|
|
49
|
+
InvalidHostError,
|
|
50
|
+
InvalidPortError,
|
|
51
|
+
InvalidPathError,
|
|
52
|
+
InvalidQueryError,
|
|
53
|
+
InvalidFragmentError,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Represents an error encountered while attempting to resolve a URI reference to a target URI.
|
|
58
|
+
*/
|
|
59
|
+
provide enum ResolveReferenceError {
|
|
60
|
+
BaseNotAbsolute,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Represents an error encountered while attempting to percent-decode a string.
|
|
65
|
+
*/
|
|
66
|
+
provide enum DecodingError {
|
|
67
|
+
InvalidEncoding,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Used to specify which characters to percent-encode from a string.
|
|
72
|
+
*/
|
|
73
|
+
provide enum EncodeSet {
|
|
74
|
+
EncodeNonUnreserved,
|
|
75
|
+
EncodeUserinfo,
|
|
76
|
+
EncodeRegisteredHost,
|
|
77
|
+
EncodePath,
|
|
78
|
+
EncodePathSegment,
|
|
79
|
+
EncodeQueryOrFragment,
|
|
80
|
+
EncodeCustom(Char => Bool),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let isHexDigit = char => {
|
|
84
|
+
use Char.{ (<=), (>=) }
|
|
85
|
+
Char.isAsciiDigit(char) ||
|
|
86
|
+
char >= 'A' && char <= 'F' ||
|
|
87
|
+
char >= 'a' && char <= 'f'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let isUnreservedChar = char => {
|
|
91
|
+
Char.isAsciiDigit(char) ||
|
|
92
|
+
Char.isAsciiAlpha(char) ||
|
|
93
|
+
char == '-' ||
|
|
94
|
+
char == '.' ||
|
|
95
|
+
char == '_' ||
|
|
96
|
+
char == '~'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let isSubDelim = char => {
|
|
100
|
+
match (char) {
|
|
101
|
+
'!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '=' => true,
|
|
102
|
+
_ => false,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let isPchar = char => {
|
|
107
|
+
isUnreservedChar(char) || isSubDelim(char) || char == ':' || char == '@'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let makeEncoder = (encodeSet: EncodeSet) => {
|
|
111
|
+
let shouldEncodeForNonUnreserved = char => !isUnreservedChar(char)
|
|
112
|
+
|
|
113
|
+
let shouldEncodeForUserinfo = char => {
|
|
114
|
+
!(isUnreservedChar(char) || isSubDelim(char) || char == ':')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let shouldEncodeForRegisteredHost = char => {
|
|
118
|
+
!(isUnreservedChar(char) || isSubDelim(char))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let shouldEncodeForPath = char => {
|
|
122
|
+
!(isPchar(char) || char == '/')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let shouldEncodeForPathSegment = char => {
|
|
126
|
+
!isPchar(char)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let shouldEncodeForQueryOrFragment = char => {
|
|
130
|
+
!(isPchar(char) || char == '/' || char == '?')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
match (encodeSet) {
|
|
134
|
+
EncodeNonUnreserved => shouldEncodeForNonUnreserved,
|
|
135
|
+
EncodeUserinfo => shouldEncodeForUserinfo,
|
|
136
|
+
EncodeRegisteredHost => shouldEncodeForRegisteredHost,
|
|
137
|
+
EncodePath => shouldEncodeForPath,
|
|
138
|
+
EncodePathSegment => shouldEncodeForPathSegment,
|
|
139
|
+
EncodeQueryOrFragment => shouldEncodeForQueryOrFragment,
|
|
140
|
+
EncodeCustom(shouldEncodeCharFn) => shouldEncodeCharFn,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let charToHexValue = char => {
|
|
145
|
+
if (Char.isAsciiDigit(char)) {
|
|
146
|
+
Char.code(char) - 0x30
|
|
147
|
+
} else {
|
|
148
|
+
let char = Char.toAsciiLowercase(char)
|
|
149
|
+
Char.code(char) - 0x60 + 9
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let hexValueToChar = val => {
|
|
154
|
+
if (val < 10) {
|
|
155
|
+
Char.fromCode(val + 0x30)
|
|
156
|
+
} else {
|
|
157
|
+
Char.fromCode(val + 0x40 - 9)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let decodeValid = (str, onlyUnreserved=false) => {
|
|
162
|
+
let bytes = String.encode(str, String.UTF8)
|
|
163
|
+
let len = Bytes.length(bytes)
|
|
164
|
+
let out = Buffer.make(len)
|
|
165
|
+
let charAt = i => Char.fromCode(Uint8.toNumber(Bytes.getUint8(i, bytes)))
|
|
166
|
+
for (let mut i = 0; i < len; i += 1) {
|
|
167
|
+
if (i >= len - 2 || charAt(i) != '%') {
|
|
168
|
+
let byte = Bytes.getUint8(i, bytes)
|
|
169
|
+
Buffer.addUint8(byte, out)
|
|
170
|
+
} else {
|
|
171
|
+
let next = charAt(i + 1)
|
|
172
|
+
let nextNext = charAt(i + 2)
|
|
173
|
+
let pctDecodedVal = charToHexValue(next) * 16 + charToHexValue(nextNext)
|
|
174
|
+
if (onlyUnreserved && !isUnreservedChar(Char.fromCode(pctDecodedVal))) {
|
|
175
|
+
Buffer.addChar('%', out)
|
|
176
|
+
Buffer.addChar(Char.toAsciiUppercase(next), out)
|
|
177
|
+
Buffer.addChar(Char.toAsciiUppercase(nextNext), out)
|
|
178
|
+
} else {
|
|
179
|
+
Buffer.addUint8(Uint8.fromNumber(pctDecodedVal), out)
|
|
180
|
+
}
|
|
181
|
+
i += 2
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
Buffer.toString(out)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let isValidEncoding = str => {
|
|
188
|
+
let chars = String.explode(str)
|
|
189
|
+
let len = Array.length(chars)
|
|
190
|
+
for (let mut i = 0; i < len; i += 1) {
|
|
191
|
+
if (
|
|
192
|
+
chars[i] == '%' &&
|
|
193
|
+
(i >= len - 2 || !isHexDigit(chars[i + 1]) || !isHexDigit(chars[i + 2]))
|
|
194
|
+
) {
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Lowercase all non-percent-encoded alphabetical characters
|
|
202
|
+
let normalizeHost = str => {
|
|
203
|
+
let str = decodeValid(str, onlyUnreserved=true)
|
|
204
|
+
|
|
205
|
+
let chars = String.explode(str)
|
|
206
|
+
let rec getChars = (i, acc) => {
|
|
207
|
+
if (i < 0) {
|
|
208
|
+
acc
|
|
209
|
+
} else if (i >= 2 && chars[i - 2] == '%') {
|
|
210
|
+
getChars(i - 3, ['%', chars[i - 1], chars[i], ...acc])
|
|
211
|
+
} else {
|
|
212
|
+
getChars(i - 1, [Char.toAsciiLowercase(chars[i]), ...acc])
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
let chars = getChars(String.length(str) - 1, [])
|
|
216
|
+
String.implode(Array.fromList(chars))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Algorithm following RFC 3986 section 5.2.4 to remove . and .. path segments
|
|
220
|
+
let removeDotSegments = path => {
|
|
221
|
+
let tail = list => {
|
|
222
|
+
match (list) {
|
|
223
|
+
[_, ...rest] => rest,
|
|
224
|
+
_ => list,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let rec traverse = (in, out) => {
|
|
229
|
+
if (in == "" || in == "." || in == "..") {
|
|
230
|
+
out
|
|
231
|
+
} else if (String.startsWith("../", in)) {
|
|
232
|
+
traverse(String.slice(3, in), out)
|
|
233
|
+
} else if (String.startsWith("./", in)) {
|
|
234
|
+
traverse(String.slice(2, in), out)
|
|
235
|
+
} else if (String.startsWith("/./", in)) {
|
|
236
|
+
traverse(String.slice(2, in), out)
|
|
237
|
+
} else if (in == "/.") {
|
|
238
|
+
traverse("/", out)
|
|
239
|
+
} else if (String.startsWith("/../", in)) {
|
|
240
|
+
traverse(String.slice(3, in), tail(out))
|
|
241
|
+
} else if (in == "/..") {
|
|
242
|
+
traverse("/", tail(out))
|
|
243
|
+
} else {
|
|
244
|
+
let (in, prefix) = if (String.charAt(0, in) == '/') {
|
|
245
|
+
(String.slice(1, in), "/")
|
|
246
|
+
} else {
|
|
247
|
+
(in, "")
|
|
248
|
+
}
|
|
249
|
+
let (segment, rest) = match (String.indexOf("/", in)) {
|
|
250
|
+
Some(i) => (String.slice(0, end=i, in), String.slice(i, in)),
|
|
251
|
+
None => (in, ""),
|
|
252
|
+
}
|
|
253
|
+
traverse(rest, [prefix ++ segment, ...out])
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
let out = traverse(path, [])
|
|
257
|
+
List.join("", List.reverse(out))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Percent-encodes characters in a string based on the specified `EncodeSet`.
|
|
262
|
+
*
|
|
263
|
+
* @param str: The string to encode
|
|
264
|
+
* @param encodeSet: An indication for which characters to percent-encode. `EncodeNonUnreserved` by default
|
|
265
|
+
* @returns A percent-encoding of the given string
|
|
266
|
+
*
|
|
267
|
+
* @example Uri.encode("h3ll0_.w ?o+rld", encodeSet=Uri.EncodeNonUnreserved) // "h3ll0_.w%20%3Fo%2Brld"
|
|
268
|
+
* @example Uri.encode("d+om@i:n.com", encodeSet=Uri.EncodeRegisteredHost) // "d+om%40i%3An.com"
|
|
269
|
+
* @example Uri.encode("word", encodeSet=Uri.EncodeCustom(c => c == 'o')) // "w%6Frd"
|
|
270
|
+
*
|
|
271
|
+
* @since v0.6.0
|
|
272
|
+
*/
|
|
273
|
+
provide let encode = (str, encodeSet=EncodeNonUnreserved) => {
|
|
274
|
+
let shouldEncode = makeEncoder(encodeSet)
|
|
275
|
+
// TODO(#2053): use String.map when implemented
|
|
276
|
+
let chars = String.explode(str)
|
|
277
|
+
let rec getChars = (i, acc) => {
|
|
278
|
+
if (i < 0) {
|
|
279
|
+
acc
|
|
280
|
+
} else {
|
|
281
|
+
let c = chars[i]
|
|
282
|
+
let acc = if (!shouldEncode(c)) {
|
|
283
|
+
[c, ...acc]
|
|
284
|
+
} else {
|
|
285
|
+
let charStr = Char.toString(c)
|
|
286
|
+
let bytes = String.encode(charStr, String.UTF8)
|
|
287
|
+
let pctEncodings = List.init(Bytes.length(bytes), i => {
|
|
288
|
+
let byte = Uint8.toNumber(Bytes.getUint8(i, bytes))
|
|
289
|
+
let firstHalf = byte >> 4
|
|
290
|
+
let secondHalf = byte & 0x000f
|
|
291
|
+
['%', hexValueToChar(firstHalf), hexValueToChar(secondHalf)]
|
|
292
|
+
})
|
|
293
|
+
List.append(List.flatten(pctEncodings), acc)
|
|
294
|
+
}
|
|
295
|
+
getChars(i - 1, acc)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
let chars = getChars(String.length(str) - 1, [])
|
|
299
|
+
String.implode(Array.fromList(chars))
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Decodes any percent-encoded characters in a string.
|
|
304
|
+
*
|
|
305
|
+
* @param str: The string to decode
|
|
306
|
+
* @returns `Ok(decoded)` containing the decoded string or `Err(err)` if the decoding failed
|
|
307
|
+
*
|
|
308
|
+
* @since v0.6.0
|
|
309
|
+
*/
|
|
310
|
+
provide let decode = str => {
|
|
311
|
+
if (isValidEncoding(str)) {
|
|
312
|
+
Ok(decodeValid(str))
|
|
313
|
+
} else {
|
|
314
|
+
Err(InvalidEncoding)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Encodes a list of key-value pairs into an query string.
|
|
320
|
+
*
|
|
321
|
+
* @param urlVals: A list of key-value pairs
|
|
322
|
+
* @returns A query string
|
|
323
|
+
*
|
|
324
|
+
* @since v0.6.0
|
|
325
|
+
*/
|
|
326
|
+
provide let encodeQuery = (urlVals, encodeSet=EncodeNonUnreserved) => {
|
|
327
|
+
let parts = List.map(((key, val)) => {
|
|
328
|
+
encode(key, encodeSet=encodeSet) ++ "=" ++ encode(val, encodeSet=encodeSet)
|
|
329
|
+
}, urlVals)
|
|
330
|
+
|
|
331
|
+
List.join("&", parts)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Decodes a query string into a list of pairs.
|
|
336
|
+
*
|
|
337
|
+
* @param str: A query string
|
|
338
|
+
* @returns `Ok(decoded)` containing a list of key-value pairs from the decoded string or `Err(err)` if the decoding failed
|
|
339
|
+
*
|
|
340
|
+
* @since v0.6.0
|
|
341
|
+
*/
|
|
342
|
+
provide let decodeQuery = str => {
|
|
343
|
+
if (isValidEncoding(str)) {
|
|
344
|
+
let parts = Array.toList(String.split("&", str))
|
|
345
|
+
Ok(List.map(part => {
|
|
346
|
+
match (String.indexOf("=", part)) {
|
|
347
|
+
// Some parts may only have a key, set value to empty string in this case
|
|
348
|
+
None => (part, ""),
|
|
349
|
+
Some(i) => {
|
|
350
|
+
let name = String.slice(0, end=i, part)
|
|
351
|
+
let val = String.slice(i + 1, part)
|
|
352
|
+
(decodeValid(name), decodeValid(val))
|
|
353
|
+
},
|
|
354
|
+
}
|
|
355
|
+
}, parts))
|
|
356
|
+
} else {
|
|
357
|
+
Err(InvalidEncoding)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
module Matchers {
|
|
362
|
+
// The functions in this module take the string being parsed and current
|
|
363
|
+
// index of the string being examined; if they are able to match with a
|
|
364
|
+
// portion of the string starting from that index they return Some(endI) with
|
|
365
|
+
// the index they scanned past, or None if they do not match successfully.
|
|
366
|
+
|
|
367
|
+
// Helpers
|
|
368
|
+
|
|
369
|
+
let charTest = test => (i, str) => {
|
|
370
|
+
if (i >= String.length(str) || !test(String.charAt(i, str))) {
|
|
371
|
+
None
|
|
372
|
+
} else {
|
|
373
|
+
Some(i + 1)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
provide let char = target => charTest(c => c == target)
|
|
378
|
+
|
|
379
|
+
provide let chars = targets => charTest(c => List.contains(c, targets))
|
|
380
|
+
|
|
381
|
+
// Akin to regex ?
|
|
382
|
+
provide let opt = scan => (i, str) => {
|
|
383
|
+
match (scan(i, str)) {
|
|
384
|
+
None => Some(i),
|
|
385
|
+
Some(i) => Some(i),
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
provide let empty = (i, _) => Some(i)
|
|
390
|
+
|
|
391
|
+
// Akin to regex *
|
|
392
|
+
provide let star = scan => {
|
|
393
|
+
let rec scanStar = (i, str) => {
|
|
394
|
+
match (scan(i, str)) {
|
|
395
|
+
None => Some(i),
|
|
396
|
+
Some(i) => scanStar(i, str),
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
scanStar
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Akin to regex +
|
|
403
|
+
provide let plus = scan => (i, str) => {
|
|
404
|
+
match (scan(i, str)) {
|
|
405
|
+
None => None,
|
|
406
|
+
Some(i) => star(scan)(i, str),
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// At most n matches of a pattern (ABNF equivalent: n*pattern)
|
|
411
|
+
let rec limit = (n, scan) => (i, str) => {
|
|
412
|
+
if (n == 0) {
|
|
413
|
+
Some(i)
|
|
414
|
+
} else {
|
|
415
|
+
match (scan(i, str)) {
|
|
416
|
+
None => Some(i),
|
|
417
|
+
Some(i) => limit(n - 1, scan)(i, str),
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
provide let digit = charTest(Char.isAsciiDigit)
|
|
423
|
+
|
|
424
|
+
provide let digitInRange = (low, high) => charTest(char => {
|
|
425
|
+
let code = Char.code(char)
|
|
426
|
+
let zero = 0x30
|
|
427
|
+
code >= zero + low && code <= zero + high
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
provide let alpha = charTest(Char.isAsciiAlpha)
|
|
431
|
+
|
|
432
|
+
provide let hexDigit = charTest(isHexDigit)
|
|
433
|
+
|
|
434
|
+
// Akin to regex |
|
|
435
|
+
provide let any = fns => (i, str) => {
|
|
436
|
+
List.reduce((acc, fn) => match (acc) {
|
|
437
|
+
None => fn(i, str),
|
|
438
|
+
x => x,
|
|
439
|
+
}, None, fns)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
provide let seq = fns => (i, str) => {
|
|
443
|
+
List.reduce((acc, fn) => match (acc) {
|
|
444
|
+
None => None,
|
|
445
|
+
Some(nextI) => fn(nextI, str),
|
|
446
|
+
}, Some(i), fns)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
provide let string = str =>
|
|
450
|
+
seq(List.map(char, Array.toList(String.explode(str))))
|
|
451
|
+
|
|
452
|
+
// Exactly N repetitions of a pattern
|
|
453
|
+
let nTimes = (n, scan) => seq(List.init(n, (_) => scan))
|
|
454
|
+
|
|
455
|
+
// Grammar rules from Appendix A of RFC 3986
|
|
456
|
+
|
|
457
|
+
provide let pctEncoded = seq([char('%'), hexDigit, hexDigit])
|
|
458
|
+
|
|
459
|
+
provide let subDelims = charTest(isSubDelim)
|
|
460
|
+
|
|
461
|
+
provide let unreserved = charTest(isUnreservedChar)
|
|
462
|
+
|
|
463
|
+
provide let pchar = any(
|
|
464
|
+
[unreserved, pctEncoded, subDelims, chars([':', '@'])]
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
provide let scheme = seq(
|
|
468
|
+
[alpha, star(any([alpha, digit, chars(['+', '-', '.'])]))]
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
provide let userinfo = star(
|
|
472
|
+
any([unreserved, pctEncoded, subDelims, char(':')])
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
let decOctet = any(
|
|
476
|
+
[
|
|
477
|
+
seq([char('2'), char('5'), digitInRange(0, 5)]),
|
|
478
|
+
seq([char('2'), digitInRange(0, 4), digit]),
|
|
479
|
+
seq([char('1'), digit, digit]),
|
|
480
|
+
seq([digitInRange(1, 9), digit]),
|
|
481
|
+
digit,
|
|
482
|
+
]
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
let ipv4Address = seq(
|
|
486
|
+
[decOctet, char('.'), decOctet, char('.'), decOctet, char('.'), decOctet]
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
let h16 = (i, str) => {
|
|
490
|
+
match (hexDigit(i, str)) {
|
|
491
|
+
None => None,
|
|
492
|
+
Some(i) => limit(3, hexDigit)(i, str),
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
let ls32 = any([seq([h16, char(':'), h16]), ipv4Address])
|
|
497
|
+
|
|
498
|
+
let ipv6Address = {
|
|
499
|
+
let h16Colon = seq([h16, char(':')])
|
|
500
|
+
let colonH16 = seq([char(':'), h16])
|
|
501
|
+
any(
|
|
502
|
+
[
|
|
503
|
+
seq([nTimes(6, h16Colon), ls32]),
|
|
504
|
+
seq([string("::"), nTimes(5, h16Colon), ls32]),
|
|
505
|
+
seq([opt(h16), string("::"), nTimes(4, h16Colon), ls32]),
|
|
506
|
+
seq(
|
|
507
|
+
[
|
|
508
|
+
opt(seq([h16, limit(1, colonH16)])),
|
|
509
|
+
string("::"),
|
|
510
|
+
nTimes(3, h16Colon),
|
|
511
|
+
ls32,
|
|
512
|
+
]
|
|
513
|
+
),
|
|
514
|
+
seq(
|
|
515
|
+
[
|
|
516
|
+
opt(seq([h16, limit(2, colonH16)])),
|
|
517
|
+
string("::"),
|
|
518
|
+
nTimes(2, h16Colon),
|
|
519
|
+
ls32,
|
|
520
|
+
]
|
|
521
|
+
),
|
|
522
|
+
seq([opt(seq([h16, limit(3, colonH16)])), string("::"), h16Colon, ls32]),
|
|
523
|
+
seq([opt(seq([h16, limit(4, colonH16)])), string("::"), ls32]),
|
|
524
|
+
seq([opt(seq([h16, limit(5, colonH16)])), string("::"), h16]),
|
|
525
|
+
seq([opt(seq([h16, limit(6, colonH16)])), string("::")]),
|
|
526
|
+
]
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let ipvFuture = seq(
|
|
531
|
+
[
|
|
532
|
+
char('v'),
|
|
533
|
+
plus(hexDigit),
|
|
534
|
+
char('.'),
|
|
535
|
+
plus(any([unreserved, subDelims, char(':')])),
|
|
536
|
+
]
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
let ipLiteral = seq([char('['), any([ipv6Address, ipvFuture]), char(']')])
|
|
540
|
+
|
|
541
|
+
provide let ipAddress = any([ipLiteral, ipv4Address])
|
|
542
|
+
|
|
543
|
+
let regName = star(any([unreserved, pctEncoded, subDelims]))
|
|
544
|
+
|
|
545
|
+
provide let host = any([ipAddress, regName])
|
|
546
|
+
|
|
547
|
+
provide let port = star(digit)
|
|
548
|
+
|
|
549
|
+
provide let pathAbempty = star(seq([char('/'), star(pchar)]))
|
|
550
|
+
|
|
551
|
+
provide let pathAbsolute = seq(
|
|
552
|
+
[char('/'), opt(seq([plus(pchar), pathAbempty]))]
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
provide let pathNoScheme = seq(
|
|
556
|
+
[plus(any([unreserved, pctEncoded, subDelims, char('@')])), pathAbempty]
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
provide let pathRootless = seq([plus(pchar), pathAbempty])
|
|
560
|
+
|
|
561
|
+
provide let query = star(any([pchar, char('/'), char('?')]))
|
|
562
|
+
|
|
563
|
+
provide let fragment = query
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
let parseScheme = (str, withDelim=false) => {
|
|
567
|
+
use Matchers.{ seq, char, scheme }
|
|
568
|
+
let matcher = if (withDelim) seq([scheme, char(':')]) else scheme
|
|
569
|
+
match (matcher(0, str)) {
|
|
570
|
+
None => (0, None),
|
|
571
|
+
Some(i) =>
|
|
572
|
+
(
|
|
573
|
+
i,
|
|
574
|
+
Some(
|
|
575
|
+
String.toAsciiLowercase(
|
|
576
|
+
String.slice(0, end=i - (if (withDelim) 1 else 0), str)
|
|
577
|
+
),
|
|
578
|
+
),
|
|
579
|
+
),
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
let parseIpAddress = (i, str) => {
|
|
584
|
+
match (Matchers.ipAddress(i, str)) {
|
|
585
|
+
None => Err(ParseError),
|
|
586
|
+
Some(endI) => Ok((endI, normalizeHost(String.slice(i, end=endI, str)))),
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
let parseHost = (i, str) => {
|
|
591
|
+
match (Matchers.host(i, str)) {
|
|
592
|
+
None => Err(ParseError),
|
|
593
|
+
Some(endI) => Ok((endI, normalizeHost(String.slice(i, end=endI, str)))),
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
let parseUserinfo = (i, str, withDelim=false) => {
|
|
598
|
+
use Matchers.{ seq, char, userinfo }
|
|
599
|
+
let matcher = if (withDelim) seq([userinfo, char('@')]) else userinfo
|
|
600
|
+
match (matcher(i, str)) {
|
|
601
|
+
None => (i, None),
|
|
602
|
+
Some(endI) =>
|
|
603
|
+
(endI, Some(String.slice(i, end=endI - (if (withDelim) 1 else 0), str))),
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
let parsePortWithDelim = (i, str) => {
|
|
608
|
+
use Matchers.{ seq, char, port }
|
|
609
|
+
match (seq([char(':'), port])(i, str)) {
|
|
610
|
+
None => (i, None),
|
|
611
|
+
Some(endI) => {
|
|
612
|
+
let port = if (endI == i + 1) {
|
|
613
|
+
None
|
|
614
|
+
} else {
|
|
615
|
+
let portStr = String.slice(i + 1, end=endI, str)
|
|
616
|
+
Some(Result.unwrap(Number.parseInt(portStr, 10)))
|
|
617
|
+
}
|
|
618
|
+
(endI, port)
|
|
619
|
+
},
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
let parsePath = (i, str, isAbsolute, hasAuthority) => {
|
|
624
|
+
let processPath = if (isAbsolute) removeDotSegments else identity
|
|
625
|
+
if (hasAuthority) {
|
|
626
|
+
let endI = Option.unwrap(Matchers.pathAbempty(i, str))
|
|
627
|
+
let path = processPath(
|
|
628
|
+
decodeValid(String.slice(i, end=endI, str), onlyUnreserved=true)
|
|
629
|
+
)
|
|
630
|
+
(endI, path)
|
|
631
|
+
} else {
|
|
632
|
+
use Matchers.{ pathRootless, pathNoScheme, pathAbsolute, empty, any }
|
|
633
|
+
let extraOption = if (isAbsolute) pathRootless else pathNoScheme
|
|
634
|
+
let endI = Option.unwrap(any([pathAbsolute, extraOption, empty])(i, str))
|
|
635
|
+
let path = processPath(
|
|
636
|
+
decodeValid(String.slice(i, end=endI, str), onlyUnreserved=true)
|
|
637
|
+
)
|
|
638
|
+
(endI, path)
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
let parseAfterScheme = (i, str, isAbsolute) => {
|
|
643
|
+
match (Matchers.string("//")(i, str)) {
|
|
644
|
+
Some(i) => {
|
|
645
|
+
let (i, userinfo) = parseUserinfo(i, str, withDelim=true)
|
|
646
|
+
let (i, host) = match (parseHost(i, str)) {
|
|
647
|
+
Ok(x) => x,
|
|
648
|
+
Err(err) => return Err(err),
|
|
649
|
+
}
|
|
650
|
+
let (i, port) = parsePortWithDelim(i, str)
|
|
651
|
+
let (i, path) = parsePath(i, str, isAbsolute, true)
|
|
652
|
+
return Ok((i, userinfo, Some(host), port, path))
|
|
653
|
+
},
|
|
654
|
+
None => {
|
|
655
|
+
let (i, path) = parsePath(i, str, isAbsolute, false)
|
|
656
|
+
return Ok((i, None, None, None, path))
|
|
657
|
+
},
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
let parseQuery = (i, str, withDelim=false) => {
|
|
662
|
+
use Matchers.{ seq, char, query }
|
|
663
|
+
let matcher = if (withDelim) seq([char('?'), query]) else query
|
|
664
|
+
match (matcher(i, str)) {
|
|
665
|
+
None => (i, None),
|
|
666
|
+
Some(endI) =>
|
|
667
|
+
(
|
|
668
|
+
endI,
|
|
669
|
+
Some(
|
|
670
|
+
decodeValid(
|
|
671
|
+
String.slice(i + (if (withDelim) 1 else 0), end=endI, str),
|
|
672
|
+
onlyUnreserved=true
|
|
673
|
+
),
|
|
674
|
+
),
|
|
675
|
+
),
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
let parseFragment = (i, str, withDelim=false) => {
|
|
680
|
+
use Matchers.{ seq, char, fragment }
|
|
681
|
+
let matcher = if (withDelim) seq([char('#'), fragment]) else fragment
|
|
682
|
+
match (matcher(i, str)) {
|
|
683
|
+
None => (i, None),
|
|
684
|
+
Some(endI) =>
|
|
685
|
+
(
|
|
686
|
+
endI,
|
|
687
|
+
Some(
|
|
688
|
+
decodeValid(
|
|
689
|
+
String.slice(i + (if (withDelim) 1 else 0), end=endI, str),
|
|
690
|
+
onlyUnreserved=true
|
|
691
|
+
),
|
|
692
|
+
),
|
|
693
|
+
),
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Parses a string into a `Uri` according to RFC 3986. If the URI string has a
|
|
699
|
+
* path it will be automatically normalized, removing unnecessary `.` and `..`
|
|
700
|
+
* segments.
|
|
701
|
+
*
|
|
702
|
+
* @param str: The RFC 3986 URI string to parse
|
|
703
|
+
* @returns `Ok(uri)` containing a `Uri` if the given string is a valid URI or `Err(ParseError)` otherwise
|
|
704
|
+
*
|
|
705
|
+
* @example Uri.parse("https://grain-lang.org") == Ok(...)
|
|
706
|
+
* @example Uri.parse("http://@*^%") == Err(Uri.ParseError)
|
|
707
|
+
*
|
|
708
|
+
* @since v0.6.0
|
|
709
|
+
*/
|
|
710
|
+
provide let parse = str => {
|
|
711
|
+
let (i, scheme) = parseScheme(str, withDelim=true)
|
|
712
|
+
let isAbsolute = Option.isSome(scheme)
|
|
713
|
+
let (i, userinfo, host, port, path) = match (
|
|
714
|
+
parseAfterScheme(i, str, isAbsolute)
|
|
715
|
+
) {
|
|
716
|
+
Ok(x) => x,
|
|
717
|
+
Err(err) => return Err(err),
|
|
718
|
+
}
|
|
719
|
+
let (i, query) = parseQuery(i, str, withDelim=true)
|
|
720
|
+
let (i, fragment) = parseFragment(i, str, withDelim=true)
|
|
721
|
+
if (i != String.length(str)) {
|
|
722
|
+
return Err(ParseError)
|
|
723
|
+
} else {
|
|
724
|
+
return Ok({ scheme, userinfo, host, port, path, query, fragment })
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Transforms a base URI and a URI reference into a target URI
|
|
730
|
+
*
|
|
731
|
+
* @param base: The base URI to resolve a URI reference on
|
|
732
|
+
* @param ref: The URI reference to apply onto the base
|
|
733
|
+
* @returns `Ok(uri)` containing the target `Uri` or `Err(err)` if the input is malformed
|
|
734
|
+
*
|
|
735
|
+
* @example resolveReference(unwrap(parse("https://grain-lang.org/docs/stdlib/uri")), unwrap(parse("../intro"))) // https://grain-lang.org/docs/intro
|
|
736
|
+
* @example resolveReference(unwrap(parse("https://grain-lang.org/docs")), unwrap(parse("?key=val"))) // https://grain-lang.org/docs?key=val
|
|
737
|
+
* @example resolveReference(unwrap(parse("https://grain-lang.org/docs")), unwrap(parse("google.com/search"))) // https://google.com/search
|
|
738
|
+
*
|
|
739
|
+
* @since v0.6.0
|
|
740
|
+
*/
|
|
741
|
+
provide let resolveReference = (base, ref) => {
|
|
742
|
+
let mergePath = (base, ref) => {
|
|
743
|
+
if (base.host != None && base.path == "") {
|
|
744
|
+
"/" ++ ref.path
|
|
745
|
+
} else {
|
|
746
|
+
let basePath = match (String.lastIndexOf("/", base.path)) {
|
|
747
|
+
Some(i) => String.slice(0, end=i + 1, base.path),
|
|
748
|
+
None => base.path,
|
|
749
|
+
}
|
|
750
|
+
basePath ++ ref.path
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (base.scheme == None) {
|
|
755
|
+
Err(BaseNotAbsolute)
|
|
756
|
+
} else {
|
|
757
|
+
let uri = if (ref.scheme != None) {
|
|
758
|
+
ref
|
|
759
|
+
} else {
|
|
760
|
+
if (ref.host != None) {
|
|
761
|
+
{ ...ref, scheme: base.scheme }
|
|
762
|
+
} else {
|
|
763
|
+
if (ref.path == "") {
|
|
764
|
+
use Option.{ (||) }
|
|
765
|
+
{ ...base, query: ref.query || base.query, fragment: ref.fragment }
|
|
766
|
+
} else {
|
|
767
|
+
let path = if (String.startsWith("/", ref.path)) {
|
|
768
|
+
ref.path
|
|
769
|
+
} else {
|
|
770
|
+
removeDotSegments(mergePath(base, ref))
|
|
771
|
+
}
|
|
772
|
+
{ ...base, path, query: ref.query, fragment: ref.fragment }
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
Ok(uri)
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Constructs a new `Uri` from components.
|
|
782
|
+
*
|
|
783
|
+
* @param scheme: `Some(scheme)` containing the desired scheme component or `None` for a scheme-less URI
|
|
784
|
+
* @param userinfo: `Some(userinfo)` containing the desired userinfo component or `None` for a userinfo-less URI
|
|
785
|
+
* @param host: `Some(host)` containing the desired host component or `None` for a host-less URI
|
|
786
|
+
* @param port: `Some(port)` containing the desired port component or `None` for a port-less URI
|
|
787
|
+
* @param path: The desired path for the URI. `""` by default
|
|
788
|
+
* @param query: `Some(query)` containing the desired query string component or `None` for a query-less URI
|
|
789
|
+
* @param fragment: `Some(fragment)` containing the desired fragment component or `None` for a fragment-less URI
|
|
790
|
+
* @param encodeComponents: Whether or not to apply percent encoding for each component to remove unsafe characters for each component
|
|
791
|
+
*
|
|
792
|
+
* @example Uri.make(scheme=Some("https"), host=Some("grain-lang.org")) // https://grain-lang.org
|
|
793
|
+
* @example Uri.make(host=Some("g/r@in"), encodeComponents=false) // Err(Uri.InvalidHostError)
|
|
794
|
+
* @example Uri.make(scheme=Some("abc"), host=Some("g/r@in"), query=Some("k/ey=v^@l"), encodeComponents=true) // abc://g%2Fr%40in?k/ey=v%5E@l
|
|
795
|
+
* @example Uri.make(port=Some(80)) // Err(Uri.PortWithNoHost)
|
|
796
|
+
*
|
|
797
|
+
* @since v0.6.0
|
|
798
|
+
*/
|
|
799
|
+
provide let make = (
|
|
800
|
+
scheme=None,
|
|
801
|
+
userinfo=None,
|
|
802
|
+
host=None,
|
|
803
|
+
port=None,
|
|
804
|
+
path="",
|
|
805
|
+
query=None,
|
|
806
|
+
fragment=None,
|
|
807
|
+
encodeComponents=false,
|
|
808
|
+
) => {
|
|
809
|
+
match ((host, userinfo, port)) {
|
|
810
|
+
(None, Some(_), None) => return Err(UserinfoWithNoHost),
|
|
811
|
+
(None, None, Some(_)) => return Err(PortWithNoHost),
|
|
812
|
+
_ => void,
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
let parseInfallible = (fn, val) => {
|
|
816
|
+
match (val) {
|
|
817
|
+
None => Ok(None),
|
|
818
|
+
Some(str) => {
|
|
819
|
+
let (i, parsed) = fn(0, str)
|
|
820
|
+
if (i != String.length(str)) Err(ParseError) else Ok(parsed)
|
|
821
|
+
},
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
let parseFallible = (fn, val) => {
|
|
826
|
+
match (val) {
|
|
827
|
+
None => Ok(None),
|
|
828
|
+
Some(str) => {
|
|
829
|
+
match (fn(0, str)) {
|
|
830
|
+
Ok((i, parsed)) => {
|
|
831
|
+
if (i != String.length(str)) Err(ParseError) else Ok(Some(parsed))
|
|
832
|
+
},
|
|
833
|
+
Err(err) => Err(err),
|
|
834
|
+
}
|
|
835
|
+
},
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
let (userinfo, host, path, query, fragment) = if (encodeComponents) {
|
|
840
|
+
let encodeOption = (val, encodeSet) =>
|
|
841
|
+
Option.map(val => encode(val, encodeSet=encodeSet), val)
|
|
842
|
+
|
|
843
|
+
let isIpAddressHost = Result.isOk(parseFallible(parseIpAddress, host))
|
|
844
|
+
|
|
845
|
+
(
|
|
846
|
+
encodeOption(userinfo, EncodeUserinfo),
|
|
847
|
+
if (!isIpAddressHost) encodeOption(host, EncodeRegisteredHost) else host,
|
|
848
|
+
encode(path, encodeSet=EncodePath),
|
|
849
|
+
encodeOption(query, EncodeQueryOrFragment),
|
|
850
|
+
encodeOption(fragment, EncodeQueryOrFragment),
|
|
851
|
+
)
|
|
852
|
+
} else {
|
|
853
|
+
(userinfo, host, path, query, fragment)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
let parseScheme = (_, x) => parseScheme(x)
|
|
857
|
+
let scheme = match (parseInfallible(parseScheme, scheme)) {
|
|
858
|
+
Ok(x) => x,
|
|
859
|
+
Err(_) => return Err(InvalidSchemeError),
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
let parseUserinfo = (i, x) => parseUserinfo(i, x)
|
|
863
|
+
let userinfo = match (parseInfallible(parseUserinfo, userinfo)) {
|
|
864
|
+
Ok(x) => x,
|
|
865
|
+
Err(_) => return Err(InvalidUserinfoError),
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
let host = match (parseFallible(parseHost, host)) {
|
|
869
|
+
Ok(x) => x,
|
|
870
|
+
Err(_) => return Err(InvalidHostError),
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
match (port) {
|
|
874
|
+
Some(port) when port < 0 || !Number.isInteger(port) =>
|
|
875
|
+
return Err(InvalidPortError),
|
|
876
|
+
_ => void,
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
let isAbsolute = Option.isSome(scheme)
|
|
880
|
+
let hasAuthority = Option.isSome(host)
|
|
881
|
+
let (i, _) = parsePath(0, path, isAbsolute, hasAuthority)
|
|
882
|
+
if (i != String.length(path)) {
|
|
883
|
+
return Err(InvalidPathError)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
let parseQuery = (i, x) => parseQuery(i, x)
|
|
887
|
+
let query = match (parseInfallible(parseQuery, query)) {
|
|
888
|
+
Ok(x) => x,
|
|
889
|
+
Err(_) => return Err(InvalidQueryError),
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
let parseFragment = (i, x) => parseFragment(i, x)
|
|
893
|
+
let fragment = match (parseInfallible(parseFragment, fragment)) {
|
|
894
|
+
Ok(x) => x,
|
|
895
|
+
Err(_) => return Err(InvalidFragmentError),
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return Ok({ scheme, userinfo, host, port, path, query, fragment })
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
enum UpdateAction<a> {
|
|
902
|
+
KeepOriginal,
|
|
903
|
+
UpdateTo(a),
|
|
904
|
+
UpdateParseError,
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Constructs a new `Uri` from a base `Uri` and components to update. The
|
|
909
|
+
* pattern used to update each component is that `None` means the base URI's
|
|
910
|
+
* component should be used and `Some(val)` means that a new value should be
|
|
911
|
+
* used for that component.
|
|
912
|
+
*
|
|
913
|
+
* @param uri: The `Uri` to update
|
|
914
|
+
* @param scheme: `Some(scheme)` containing the desired updated scheme component or `None` to maintain the base URI's scheme
|
|
915
|
+
* @param userinfo: `Some(userinfo)` containing the desired updated userinfo component or `None` to maintain the base URI's userinfo
|
|
916
|
+
* @param host: `Some(host)` containing the desired updated host component or `None` to maintain the base URI's host
|
|
917
|
+
* @param port: `Some(port)` containing the desired updated port component or `None` to maintain the base URI's port
|
|
918
|
+
* @param path: `Some(path)` containing the desired updated path component or `None` to maintain the base URI's path
|
|
919
|
+
* @param query: `Some(query)` containing the desired updated query string component or `None` to maintain the base URI's query
|
|
920
|
+
* @param fragment: `Some(fragment)` containing the desired updated fragment component or `None` to maintain the base URI's fragment
|
|
921
|
+
* @param encodeComponents: Whether or not to apply percent encoding for each updated component to remove unsafe characters
|
|
922
|
+
*
|
|
923
|
+
* @example let uri = Result.unwrap(Uri.parse("https://grain-lang.org/docs?k=v")) // Base URI for following examples
|
|
924
|
+
* @example Uri.update(uri, scheme=Some(Some("ftp"))) // ftp://grain-lang.org/docs?k=v
|
|
925
|
+
* @example Uri.update(uri, query=Some(None)) // https://grain-lang.org/docs
|
|
926
|
+
* @example Uri.update(uri, host=Some(Some("g/r@in")), encodeComponents=true) // https://g%2Fr%40in/docs?k=v
|
|
927
|
+
* @example Uri.update(uri, host=Some(None), port=Some(Some(80))) // Err(Uri.PortWithNoHost)
|
|
928
|
+
*
|
|
929
|
+
* @since v0.6.0
|
|
930
|
+
*/
|
|
931
|
+
provide let update = (
|
|
932
|
+
uri,
|
|
933
|
+
scheme=None,
|
|
934
|
+
userinfo=None,
|
|
935
|
+
host=None,
|
|
936
|
+
port=None,
|
|
937
|
+
path=None,
|
|
938
|
+
query=None,
|
|
939
|
+
fragment=None,
|
|
940
|
+
encodeComponents=false,
|
|
941
|
+
) => {
|
|
942
|
+
let (??) = (new, old) => Option.unwrapWithDefault(old, new)
|
|
943
|
+
match ((host ?? uri.host, userinfo ?? uri.userinfo, port ?? uri.port)) {
|
|
944
|
+
(None, Some(_), None) => return Err(UserinfoWithNoHost),
|
|
945
|
+
(None, None, Some(_)) => return Err(PortWithNoHost),
|
|
946
|
+
_ => void,
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
let parseInfallible = (fn, val) => {
|
|
950
|
+
match (val) {
|
|
951
|
+
None => KeepOriginal,
|
|
952
|
+
Some(None) => UpdateTo(None),
|
|
953
|
+
Some(Some(str)) => {
|
|
954
|
+
let (i, parsed) = fn(0, str)
|
|
955
|
+
if (i != String.length(str)) UpdateParseError else UpdateTo(parsed)
|
|
956
|
+
},
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
let parseFallible = (fn, val) => {
|
|
961
|
+
match (val) {
|
|
962
|
+
None => KeepOriginal,
|
|
963
|
+
Some(None) => UpdateTo(None),
|
|
964
|
+
Some(Some(str)) => {
|
|
965
|
+
match (fn(0, str)) {
|
|
966
|
+
Ok((i, parsed)) => {
|
|
967
|
+
if (i != String.length(str))
|
|
968
|
+
UpdateParseError
|
|
969
|
+
else
|
|
970
|
+
UpdateTo(Some(parsed))
|
|
971
|
+
},
|
|
972
|
+
Err(err) => UpdateParseError,
|
|
973
|
+
}
|
|
974
|
+
},
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
let (userinfo, host, path, query, fragment) = if (encodeComponents) {
|
|
979
|
+
let encodeOption = (val, encodeSet) => match (val) {
|
|
980
|
+
Some(Some(val)) => Some(Some(encode(val, encodeSet=encodeSet))),
|
|
981
|
+
val => val,
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
let isIpAddressHost = match (parseFallible(parseIpAddress, host)) {
|
|
985
|
+
UpdateParseError => false,
|
|
986
|
+
_ => true,
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
(
|
|
990
|
+
encodeOption(userinfo, EncodeUserinfo),
|
|
991
|
+
if (!isIpAddressHost) encodeOption(host, EncodeRegisteredHost) else host,
|
|
992
|
+
Option.map(path => encode(path, encodeSet=EncodePath), path),
|
|
993
|
+
encodeOption(query, EncodeQueryOrFragment),
|
|
994
|
+
encodeOption(fragment, EncodeQueryOrFragment),
|
|
995
|
+
)
|
|
996
|
+
} else {
|
|
997
|
+
(userinfo, host, path, query, fragment)
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
let parseScheme = (_, x) => parseScheme(x)
|
|
1001
|
+
let scheme = match (parseInfallible(parseScheme, scheme)) {
|
|
1002
|
+
KeepOriginal => uri.scheme,
|
|
1003
|
+
UpdateTo(x) => x,
|
|
1004
|
+
UpdateParseError => return Err(InvalidSchemeError),
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
let parseUserinfo = (i, x) => parseUserinfo(i, x)
|
|
1008
|
+
let userinfo = match (parseInfallible(parseUserinfo, userinfo)) {
|
|
1009
|
+
KeepOriginal => uri.userinfo,
|
|
1010
|
+
UpdateTo(x) => x,
|
|
1011
|
+
UpdateParseError => return Err(InvalidUserinfoError),
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
let host = match (parseFallible(parseHost, host)) {
|
|
1015
|
+
KeepOriginal => uri.host,
|
|
1016
|
+
UpdateTo(x) => x,
|
|
1017
|
+
UpdateParseError => return Err(InvalidHostError),
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
let port = match (port) {
|
|
1021
|
+
None => uri.port,
|
|
1022
|
+
Some(Some(port)) when port < 0 || !Number.isInteger(port) =>
|
|
1023
|
+
return Err(InvalidPortError),
|
|
1024
|
+
Some(port) => port,
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
let hasAuthority = Option.isSome(host)
|
|
1028
|
+
let isAbsolute = Option.isSome(scheme)
|
|
1029
|
+
let path = path ?? uri.path
|
|
1030
|
+
// We also want to catch situations where the path isn't directly updated but
|
|
1031
|
+
// the host or scheme is, resulting in the path no longer being valid
|
|
1032
|
+
let (i, _) = parsePath(0, path, isAbsolute, hasAuthority)
|
|
1033
|
+
if (i != String.length(path)) {
|
|
1034
|
+
return Err(InvalidPathError)
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
let parseQuery = (i, x) => parseQuery(i, x)
|
|
1038
|
+
let query = match (parseInfallible(parseQuery, query)) {
|
|
1039
|
+
KeepOriginal => uri.query,
|
|
1040
|
+
UpdateTo(x) => x,
|
|
1041
|
+
UpdateParseError => return Err(InvalidQueryError),
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
let parseFragment = (i, x) => parseFragment(i, x)
|
|
1045
|
+
let fragment = match (parseInfallible(parseFragment, fragment)) {
|
|
1046
|
+
KeepOriginal => uri.fragment,
|
|
1047
|
+
UpdateTo(x) => x,
|
|
1048
|
+
UpdateParseError => return Err(InvalidFragmentError),
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
return Ok({ scheme, userinfo, host, port, path, query, fragment })
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Determines whether a `Uri` has an authority (i.e. has a host component)
|
|
1056
|
+
*
|
|
1057
|
+
* @param uri: The `Uri` to consider
|
|
1058
|
+
* @returns `true` if the `Uri` has an authority component or `false` otherwise
|
|
1059
|
+
*
|
|
1060
|
+
* @since v0.6.0
|
|
1061
|
+
*/
|
|
1062
|
+
provide let hasAuthority = uri => uri.host != None
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Determines whether a `Uri` is an absolute URI (has a scheme component)
|
|
1066
|
+
*
|
|
1067
|
+
* @param uri: The `Uri` to consider
|
|
1068
|
+
* @returns `true` if the `Uri` is absolute (has a scheme component) or `false` otherwise
|
|
1069
|
+
*
|
|
1070
|
+
* @since v0.6.0
|
|
1071
|
+
*/
|
|
1072
|
+
provide let isAbsolute = uri => uri.scheme != None
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Converts the given `Uri` into a string.
|
|
1076
|
+
*
|
|
1077
|
+
* @param uri: The `Uri` to convert
|
|
1078
|
+
* @returns A string representation of the `Uri`
|
|
1079
|
+
*
|
|
1080
|
+
* @since v0.6.0
|
|
1081
|
+
*/
|
|
1082
|
+
provide let toString = uri => {
|
|
1083
|
+
let optStr = (opt, display) => Option.mapWithDefault(display, "", opt)
|
|
1084
|
+
|
|
1085
|
+
optStr(uri.scheme, s => s ++ ":") ++
|
|
1086
|
+
optStr(uri.host, (_) => "//") ++
|
|
1087
|
+
optStr(uri.userinfo, u => u ++ "@") ++
|
|
1088
|
+
optStr(uri.host, identity) ++
|
|
1089
|
+
optStr(uri.port, p => ":" ++ toString(p)) ++
|
|
1090
|
+
uri.path ++
|
|
1091
|
+
optStr(uri.query, q => "?" ++ q) ++
|
|
1092
|
+
optStr(uri.fragment, f => "#" ++ f)
|
|
1093
|
+
}
|