@grain/stdlib 0.5.13 → 0.6.0

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.
Files changed (155) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/LICENSE +1 -1
  3. package/README.md +25 -2
  4. package/array.gr +1512 -199
  5. package/array.md +2032 -94
  6. package/bigint.gr +239 -140
  7. package/bigint.md +450 -106
  8. package/buffer.gr +595 -102
  9. package/buffer.md +903 -145
  10. package/bytes.gr +401 -110
  11. package/bytes.md +551 -63
  12. package/char.gr +228 -49
  13. package/char.md +373 -7
  14. package/exception.gr +26 -12
  15. package/exception.md +29 -5
  16. package/float32.gr +130 -109
  17. package/float32.md +185 -57
  18. package/float64.gr +112 -99
  19. package/float64.md +185 -57
  20. package/hash.gr +47 -37
  21. package/hash.md +21 -3
  22. package/int16.gr +430 -0
  23. package/int16.md +618 -0
  24. package/int32.gr +200 -269
  25. package/int32.md +254 -289
  26. package/int64.gr +142 -225
  27. package/int64.md +254 -289
  28. package/int8.gr +511 -0
  29. package/int8.md +786 -0
  30. package/json.gr +2084 -0
  31. package/json.md +608 -0
  32. package/list.gr +120 -68
  33. package/list.md +125 -80
  34. package/map.gr +560 -57
  35. package/map.md +672 -56
  36. package/marshal.gr +239 -227
  37. package/marshal.md +36 -4
  38. package/number.gr +626 -676
  39. package/number.md +738 -153
  40. package/option.gr +33 -35
  41. package/option.md +58 -42
  42. package/package.json +2 -2
  43. package/path.gr +148 -187
  44. package/path.md +47 -96
  45. package/pervasives.gr +75 -416
  46. package/pervasives.md +85 -180
  47. package/priorityqueue.gr +433 -74
  48. package/priorityqueue.md +422 -54
  49. package/queue.gr +362 -80
  50. package/queue.md +433 -38
  51. package/random.gr +67 -75
  52. package/random.md +68 -40
  53. package/range.gr +135 -63
  54. package/range.md +198 -43
  55. package/rational.gr +284 -0
  56. package/rational.md +545 -0
  57. package/regex.gr +933 -1066
  58. package/regex.md +59 -60
  59. package/result.gr +23 -25
  60. package/result.md +54 -39
  61. package/runtime/atof/common.gr +78 -82
  62. package/runtime/atof/common.md +22 -10
  63. package/runtime/atof/decimal.gr +102 -127
  64. package/runtime/atof/decimal.md +28 -7
  65. package/runtime/atof/lemire.gr +56 -71
  66. package/runtime/atof/lemire.md +9 -1
  67. package/runtime/atof/parse.gr +83 -110
  68. package/runtime/atof/parse.md +12 -2
  69. package/runtime/atof/slow.gr +28 -35
  70. package/runtime/atof/slow.md +9 -1
  71. package/runtime/atof/table.gr +19 -18
  72. package/runtime/atof/table.md +10 -2
  73. package/runtime/atoi/parse.gr +153 -136
  74. package/runtime/atoi/parse.md +50 -1
  75. package/runtime/bigint.gr +410 -517
  76. package/runtime/bigint.md +71 -57
  77. package/runtime/compare.gr +176 -85
  78. package/runtime/compare.md +31 -1
  79. package/runtime/dataStructures.gr +144 -32
  80. package/runtime/dataStructures.md +267 -31
  81. package/runtime/debugPrint.gr +34 -15
  82. package/runtime/debugPrint.md +37 -5
  83. package/runtime/equal.gr +53 -52
  84. package/runtime/equal.md +30 -1
  85. package/runtime/exception.gr +38 -47
  86. package/runtime/exception.md +10 -8
  87. package/runtime/gc.gr +23 -152
  88. package/runtime/gc.md +13 -17
  89. package/runtime/malloc.gr +31 -31
  90. package/runtime/malloc.md +11 -3
  91. package/runtime/numberUtils.gr +191 -172
  92. package/runtime/numberUtils.md +17 -9
  93. package/runtime/numbers.gr +1695 -1021
  94. package/runtime/numbers.md +1098 -134
  95. package/runtime/string.gr +540 -242
  96. package/runtime/string.md +76 -6
  97. package/runtime/unsafe/constants.gr +30 -13
  98. package/runtime/unsafe/constants.md +80 -0
  99. package/runtime/unsafe/conv.gr +55 -28
  100. package/runtime/unsafe/conv.md +41 -9
  101. package/runtime/unsafe/memory.gr +10 -30
  102. package/runtime/unsafe/memory.md +15 -19
  103. package/runtime/unsafe/tags.gr +37 -21
  104. package/runtime/unsafe/tags.md +88 -8
  105. package/runtime/unsafe/wasmf32.gr +30 -36
  106. package/runtime/unsafe/wasmf32.md +64 -56
  107. package/runtime/unsafe/wasmf64.gr +30 -36
  108. package/runtime/unsafe/wasmf64.md +64 -56
  109. package/runtime/unsafe/wasmi32.gr +49 -66
  110. package/runtime/unsafe/wasmi32.md +102 -94
  111. package/runtime/unsafe/wasmi64.gr +52 -79
  112. package/runtime/unsafe/wasmi64.md +108 -100
  113. package/runtime/utils/printing.gr +13 -15
  114. package/runtime/utils/printing.md +11 -3
  115. package/runtime/wasi.gr +294 -295
  116. package/runtime/wasi.md +62 -42
  117. package/set.gr +574 -64
  118. package/set.md +634 -54
  119. package/stack.gr +181 -64
  120. package/stack.md +271 -42
  121. package/string.gr +453 -533
  122. package/string.md +241 -151
  123. package/uint16.gr +369 -0
  124. package/uint16.md +585 -0
  125. package/uint32.gr +470 -0
  126. package/uint32.md +737 -0
  127. package/uint64.gr +471 -0
  128. package/uint64.md +737 -0
  129. package/uint8.gr +369 -0
  130. package/uint8.md +585 -0
  131. package/uri.gr +1093 -0
  132. package/uri.md +477 -0
  133. package/{sys → wasi}/file.gr +914 -500
  134. package/{sys → wasi}/file.md +454 -50
  135. package/wasi/process.gr +292 -0
  136. package/{sys → wasi}/process.md +164 -6
  137. package/wasi/random.gr +77 -0
  138. package/wasi/random.md +80 -0
  139. package/{sys → wasi}/time.gr +15 -22
  140. package/{sys → wasi}/time.md +5 -5
  141. package/immutablearray.gr +0 -929
  142. package/immutablearray.md +0 -1038
  143. package/immutablemap.gr +0 -493
  144. package/immutablemap.md +0 -479
  145. package/immutablepriorityqueue.gr +0 -360
  146. package/immutablepriorityqueue.md +0 -291
  147. package/immutableset.gr +0 -498
  148. package/immutableset.md +0 -449
  149. package/runtime/debug.gr +0 -2
  150. package/runtime/debug.md +0 -6
  151. package/runtime/unsafe/errors.gr +0 -36
  152. package/runtime/unsafe/errors.md +0 -204
  153. package/sys/process.gr +0 -254
  154. package/sys/random.gr +0 -79
  155. 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
+ }