@bessemer/cornerstone 0.5.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.
- package/jest.config.js +3 -0
- package/package.json +39 -0
- package/src/array.ts +142 -0
- package/src/async.ts +114 -0
- package/src/cache.ts +236 -0
- package/src/combinable.ts +40 -0
- package/src/comparator.ts +78 -0
- package/src/content.ts +138 -0
- package/src/context.ts +6 -0
- package/src/crypto.ts +11 -0
- package/src/date.ts +18 -0
- package/src/duration.ts +57 -0
- package/src/either.ts +29 -0
- package/src/entry.ts +21 -0
- package/src/equalitor.ts +12 -0
- package/src/error-event.ts +126 -0
- package/src/error.ts +16 -0
- package/src/expression/array-expression.ts +29 -0
- package/src/expression/expression-evaluator.ts +34 -0
- package/src/expression/expression.ts +188 -0
- package/src/expression/internal.ts +34 -0
- package/src/expression/numeric-expression.ts +182 -0
- package/src/expression/string-expression.ts +38 -0
- package/src/expression.ts +48 -0
- package/src/function.ts +3 -0
- package/src/glob.ts +19 -0
- package/src/global-variable.ts +40 -0
- package/src/hash.ts +28 -0
- package/src/hex-code.ts +6 -0
- package/src/index.ts +82 -0
- package/src/lazy.ts +11 -0
- package/src/logger.ts +144 -0
- package/src/math.ts +132 -0
- package/src/misc.ts +22 -0
- package/src/object.ts +236 -0
- package/src/patch.ts +128 -0
- package/src/precondition.ts +25 -0
- package/src/promise.ts +16 -0
- package/src/property.ts +29 -0
- package/src/reference.ts +68 -0
- package/src/resource.ts +32 -0
- package/src/result.ts +66 -0
- package/src/retry.ts +70 -0
- package/src/rich-text.ts +24 -0
- package/src/set.ts +46 -0
- package/src/signature.ts +20 -0
- package/src/store.ts +91 -0
- package/src/string.ts +173 -0
- package/src/tag.ts +68 -0
- package/src/types.ts +21 -0
- package/src/ulid.ts +28 -0
- package/src/unit.ts +4 -0
- package/src/uri.ts +321 -0
- package/src/url.ts +155 -0
- package/src/uuid.ts +37 -0
- package/src/zod.ts +24 -0
- package/test/comparator.test.ts +1 -0
- package/test/expression.test.ts +12 -0
- package/test/object.test.ts +104 -0
- package/test/patch.test.ts +170 -0
- package/test/set.test.ts +20 -0
- package/test/string.test.ts +22 -0
- package/test/uri.test.ts +111 -0
- package/test/url.test.ts +174 -0
- package/tsconfig.build.json +13 -0
- package/tsup.config.ts +4 -0
package/src/types.ts
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
export interface NominalTyping<NominalTypingT> {
|
2
|
+
_type?: NominalTypingT
|
3
|
+
}
|
4
|
+
|
5
|
+
export type NominalType<T, NominalTypingT> = T & NominalTyping<NominalTypingT>
|
6
|
+
|
7
|
+
interface TaggedTyping<TaggedTypingT> {
|
8
|
+
_type: TaggedTypingT
|
9
|
+
}
|
10
|
+
|
11
|
+
export type TaggedType<T, TaggedTypingT> = T & TaggedTyping<TaggedTypingT>
|
12
|
+
|
13
|
+
export type Throwable = unknown
|
14
|
+
|
15
|
+
export type Dictionary<T> = Record<string, T>
|
16
|
+
|
17
|
+
export type Nil = null | undefined
|
18
|
+
|
19
|
+
export type DeepPartial<T> = {
|
20
|
+
[P in keyof T]?: T[P] extends Array<infer U> ? DeepPartial<U>[] : T[P] extends object | undefined ? DeepPartial<T[P]> : T[P]
|
21
|
+
}
|
package/src/ulid.ts
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
import { ulid } from 'ulid'
|
2
|
+
import { TaggedType } from '@bessemer/cornerstone/types'
|
3
|
+
import { Preconditions, Strings } from '@bessemer/cornerstone/index'
|
4
|
+
|
5
|
+
export type Ulid = TaggedType<string, 'Ulid'>
|
6
|
+
|
7
|
+
export const generate = (): Ulid => {
|
8
|
+
return ulid() as Ulid
|
9
|
+
}
|
10
|
+
|
11
|
+
export const asString = (value: Ulid): string => {
|
12
|
+
return value
|
13
|
+
}
|
14
|
+
|
15
|
+
export const generateString = (): string => {
|
16
|
+
return asString(generate())
|
17
|
+
}
|
18
|
+
|
19
|
+
export const of = (value: string): Ulid => {
|
20
|
+
Preconditions.isTrue(isValid(value), () => `Attempted to coerce invalid value to Ulid: ${value}`)
|
21
|
+
return value as Ulid
|
22
|
+
}
|
23
|
+
|
24
|
+
const pattern = /^[0-9A-HJKMNP-TV-Z]{26}$/
|
25
|
+
|
26
|
+
export const isValid = (value: unknown): value is Ulid => {
|
27
|
+
return Strings.isString(value) && pattern.test(value)
|
28
|
+
}
|
package/src/unit.ts
ADDED
package/src/uri.ts
ADDED
@@ -0,0 +1,321 @@
|
|
1
|
+
import { Objects, Strings } from '@bessemer/cornerstone'
|
2
|
+
import { NominalType } from '@bessemer/cornerstone/types'
|
3
|
+
import { StringSplitResult } from '@bessemer/cornerstone/string'
|
4
|
+
|
5
|
+
export const encode = (uriComponent: UriComponent) => {
|
6
|
+
return encodeURIComponent(uriComponent)
|
7
|
+
}
|
8
|
+
|
9
|
+
export const decode = (uriComponent: UriComponent) => {
|
10
|
+
return decodeURIComponent(uriComponent)
|
11
|
+
}
|
12
|
+
|
13
|
+
export type UriString = NominalType<string, 'UriString'>
|
14
|
+
export type UriComponent = string
|
15
|
+
|
16
|
+
export type UriScheme = string
|
17
|
+
|
18
|
+
export type UriAuthentication = {
|
19
|
+
principal: string
|
20
|
+
password: string | null
|
21
|
+
}
|
22
|
+
|
23
|
+
export type UriHost = {
|
24
|
+
value: string
|
25
|
+
port: number | null
|
26
|
+
}
|
27
|
+
|
28
|
+
export interface UriLocation {
|
29
|
+
path: string
|
30
|
+
query: string | null
|
31
|
+
fragment: string | null
|
32
|
+
}
|
33
|
+
|
34
|
+
export interface Uri {
|
35
|
+
scheme: UriScheme | null
|
36
|
+
host: UriHost | null
|
37
|
+
authentication: UriAuthentication | null
|
38
|
+
location: UriLocation
|
39
|
+
}
|
40
|
+
|
41
|
+
const parseSchemePart = (url: UriComponent): [UriScheme | null, UriComponent] => {
|
42
|
+
// Search for the colon or double slash
|
43
|
+
const schemeMatch = Strings.splitFirst(url, /(\/\/|:)/)
|
44
|
+
|
45
|
+
// If we don't find either, or we hit the double slash before finding a colon, there is no scheme
|
46
|
+
if (Objects.isNil(schemeMatch.selection) || schemeMatch.separator === '//') {
|
47
|
+
return [null, url]
|
48
|
+
}
|
49
|
+
|
50
|
+
// This means the string started with :, so no protocol. We'll go ahead and remove the : from consideration
|
51
|
+
if (Strings.isEmpty(schemeMatch.selection)) {
|
52
|
+
return [null, schemeMatch.rest]
|
53
|
+
} else {
|
54
|
+
return [schemeMatch.selection, schemeMatch.rest]
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
const parseAuthenticationPart = (url: UriComponent): [UriAuthentication | null, UriComponent] => {
|
59
|
+
let targetPart = url
|
60
|
+
const queryMatch = Strings.splitFirst(targetPart, '?')
|
61
|
+
const fragmentMatch = Strings.splitFirst(targetPart, '#')
|
62
|
+
if (Objects.isPresent(queryMatch.selection)) {
|
63
|
+
targetPart = queryMatch.selection
|
64
|
+
} else if (Objects.isPresent(fragmentMatch.selection)) {
|
65
|
+
targetPart = fragmentMatch.selection
|
66
|
+
}
|
67
|
+
|
68
|
+
const { selection: authentication } = Strings.splitFirst(targetPart, '@')
|
69
|
+
|
70
|
+
// If there is no @, then we don't have an authentication
|
71
|
+
if (Objects.isNil(authentication)) {
|
72
|
+
return [null, url]
|
73
|
+
}
|
74
|
+
|
75
|
+
const { rest } = Strings.splitFirst(url, '@')
|
76
|
+
|
77
|
+
return [parseAuthentication(Strings.removeStart(authentication, '//')), '//' + rest]
|
78
|
+
}
|
79
|
+
|
80
|
+
const parseAuthentication = (authentication: UriComponent): UriAuthentication => {
|
81
|
+
const { selection: principal, rest: authenticationRest } = Strings.splitFirst(authentication, ':')
|
82
|
+
|
83
|
+
// If there isn't a colon, then there is no password but there is a username
|
84
|
+
if (Objects.isNil(principal)) {
|
85
|
+
return { principal: authenticationRest, password: null }
|
86
|
+
}
|
87
|
+
|
88
|
+
// The authentication section started with a :, don't know what to make of this... password but no username?
|
89
|
+
if (Strings.isEmpty(principal)) {
|
90
|
+
throw new Error(`Unable to parse Authentication: ${authentication}`)
|
91
|
+
}
|
92
|
+
|
93
|
+
// Otherwise, we have both, so return the complete authentication object and the rest
|
94
|
+
return { principal, password: authenticationRest }
|
95
|
+
}
|
96
|
+
|
97
|
+
const parseHostPart = (url: UriComponent): [UriHost | null, UriComponent] => {
|
98
|
+
// Check if the host is starting with reserved characters, if so we should just bail on trying to parse
|
99
|
+
if (Strings.startsWith(url, '?') || Strings.startsWith(url, '#')) {
|
100
|
+
return [null, url]
|
101
|
+
}
|
102
|
+
|
103
|
+
let hostRequired = Strings.startsWith(url, '//')
|
104
|
+
if (!hostRequired) {
|
105
|
+
return [null, url]
|
106
|
+
}
|
107
|
+
|
108
|
+
url = Strings.removeStart(url, '//')
|
109
|
+
|
110
|
+
// Lets grab everything to the left of the first / ? or #, this is the remainder of our authority (if any)
|
111
|
+
const urlMatch = Strings.splitFirst(url, /[\/?#]/)
|
112
|
+
let host = urlMatch.rest
|
113
|
+
let rest = ''
|
114
|
+
|
115
|
+
if (Objects.isPresent(urlMatch.selection)) {
|
116
|
+
host = urlMatch.selection
|
117
|
+
rest = urlMatch.separator + urlMatch.rest
|
118
|
+
}
|
119
|
+
|
120
|
+
if (Strings.isEmpty(host)) {
|
121
|
+
return [null, rest]
|
122
|
+
}
|
123
|
+
|
124
|
+
return [parseHost(host), rest]
|
125
|
+
}
|
126
|
+
|
127
|
+
const parseHost = (host: UriComponent): UriHost => {
|
128
|
+
// Try to see if we have an ipv6 address like the form [2001:db8::7] and handle it
|
129
|
+
if (Strings.startsWith(host, '[')) {
|
130
|
+
const ipMatch = Strings.splitFirst(host, ']')
|
131
|
+
|
132
|
+
if (Objects.isPresent(ipMatch.selection)) {
|
133
|
+
const portMatch = Strings.splitFirst(ipMatch.rest, ':')
|
134
|
+
if (Objects.isPresent(portMatch.selection)) {
|
135
|
+
if (Strings.isEmpty(portMatch.selection)) {
|
136
|
+
return { value: ipMatch.selection + ']', port: Number(portMatch.rest) }
|
137
|
+
}
|
138
|
+
} else {
|
139
|
+
return { value: ipMatch.selection + ']', port: null }
|
140
|
+
}
|
141
|
+
}
|
142
|
+
}
|
143
|
+
|
144
|
+
let hostMatch: StringSplitResult = Strings.splitFirst(host, ':')
|
145
|
+
|
146
|
+
// We have no :, which means no port, so treat the rest as the hostname
|
147
|
+
if (Objects.isNil(hostMatch.selection)) {
|
148
|
+
return { value: hostMatch.rest, port: null }
|
149
|
+
}
|
150
|
+
|
151
|
+
// The host started with a :, this is odd
|
152
|
+
if (Strings.isEmpty(hostMatch.selection)) {
|
153
|
+
throw new Error(`Unable to parse Host: ${host}`)
|
154
|
+
}
|
155
|
+
|
156
|
+
const hostName = hostMatch.selection
|
157
|
+
|
158
|
+
// Otherwise, we have both, so return the complete authentication object and the rest
|
159
|
+
return { value: hostName, port: Number(hostMatch.rest) }
|
160
|
+
}
|
161
|
+
|
162
|
+
const parseLocation = (url: UriComponent): UriLocation => {
|
163
|
+
const location: UriLocation = { path: '', query: null, fragment: null }
|
164
|
+
|
165
|
+
// Lets see if we have a fragment and parse it off the end
|
166
|
+
const fragmentMatch = Strings.splitFirst(url, '#')
|
167
|
+
if (Objects.isPresent(fragmentMatch.selection) && !Strings.isEmpty(fragmentMatch.rest)) {
|
168
|
+
location.fragment = fragmentMatch.rest
|
169
|
+
}
|
170
|
+
|
171
|
+
// Lets see if we have a query string and parse it off the remainder
|
172
|
+
const queryMatch = Strings.splitFirst(fragmentMatch.selection ?? fragmentMatch.rest, '?')
|
173
|
+
if (Objects.isPresent(queryMatch.selection) && !Strings.isEmpty(queryMatch.rest)) {
|
174
|
+
location.query = queryMatch.rest
|
175
|
+
}
|
176
|
+
|
177
|
+
location.path = queryMatch.selection ?? queryMatch.rest
|
178
|
+
return location
|
179
|
+
}
|
180
|
+
|
181
|
+
export const parse = (urlString: UriString): Uri => {
|
182
|
+
const [scheme, rest1] = parseSchemePart(urlString)
|
183
|
+
const [authentication, rest2] = parseAuthenticationPart(rest1)
|
184
|
+
const [host, rest3] = parseHostPart(rest2)
|
185
|
+
const location = parseLocation(rest3)
|
186
|
+
const url: Uri = { scheme, host, authentication, location }
|
187
|
+
return url
|
188
|
+
}
|
189
|
+
|
190
|
+
export const emptyLocation = (): UriLocation => {
|
191
|
+
return {
|
192
|
+
path: '',
|
193
|
+
query: null,
|
194
|
+
fragment: null,
|
195
|
+
}
|
196
|
+
}
|
197
|
+
|
198
|
+
export type UriBuilder = {
|
199
|
+
scheme?: string
|
200
|
+
host?:
|
201
|
+
| {
|
202
|
+
value: string
|
203
|
+
port?: number
|
204
|
+
}
|
205
|
+
| string
|
206
|
+
authentication?:
|
207
|
+
| {
|
208
|
+
principal: string
|
209
|
+
password?: string
|
210
|
+
}
|
211
|
+
| string
|
212
|
+
location?:
|
213
|
+
| {
|
214
|
+
path: string
|
215
|
+
query?: string
|
216
|
+
fragment?: string
|
217
|
+
}
|
218
|
+
| string
|
219
|
+
}
|
220
|
+
|
221
|
+
export const build = (builder: UriBuilder): Uri => {
|
222
|
+
const scheme = builder.scheme ?? null
|
223
|
+
|
224
|
+
let host: UriHost | null = null
|
225
|
+
if (Objects.isPresent(builder.host)) {
|
226
|
+
if (Strings.isString(builder.host)) {
|
227
|
+
host = parseHost(builder.host)
|
228
|
+
} else {
|
229
|
+
host = {
|
230
|
+
value: builder.host.value,
|
231
|
+
port: builder.host.port ?? null,
|
232
|
+
}
|
233
|
+
}
|
234
|
+
}
|
235
|
+
|
236
|
+
let authentication: UriAuthentication | null = null
|
237
|
+
if (Objects.isPresent(builder.authentication)) {
|
238
|
+
if (Strings.isString(builder.authentication)) {
|
239
|
+
authentication = parseAuthentication(builder.authentication)
|
240
|
+
} else {
|
241
|
+
authentication = {
|
242
|
+
principal: builder.authentication.principal,
|
243
|
+
password: builder.authentication.password ?? null,
|
244
|
+
}
|
245
|
+
}
|
246
|
+
}
|
247
|
+
|
248
|
+
let location: UriLocation = emptyLocation()
|
249
|
+
if (Objects.isPresent(builder.location)) {
|
250
|
+
if (Strings.isString(builder.location)) {
|
251
|
+
location = parseLocation(builder.location)
|
252
|
+
} else {
|
253
|
+
location = {
|
254
|
+
path: builder.location.path,
|
255
|
+
query: builder.location.query ?? null,
|
256
|
+
fragment: builder.location.fragment ?? null,
|
257
|
+
}
|
258
|
+
}
|
259
|
+
}
|
260
|
+
|
261
|
+
return {
|
262
|
+
scheme,
|
263
|
+
host,
|
264
|
+
authentication,
|
265
|
+
location,
|
266
|
+
}
|
267
|
+
}
|
268
|
+
|
269
|
+
export const format = (uri: Uri): UriString => {
|
270
|
+
let urlString = ''
|
271
|
+
if (Objects.isPresent(uri.scheme)) {
|
272
|
+
urlString = urlString + uri.scheme
|
273
|
+
}
|
274
|
+
|
275
|
+
if (Objects.isPresent(uri.host)) {
|
276
|
+
if (Objects.isPresent(uri.scheme)) {
|
277
|
+
urlString = urlString + '://'
|
278
|
+
}
|
279
|
+
|
280
|
+
if (Objects.isPresent(uri.authentication)) {
|
281
|
+
urlString = urlString + uri.authentication.principal
|
282
|
+
|
283
|
+
if (Objects.isPresent(uri.authentication.password)) {
|
284
|
+
urlString = urlString + ':' + uri.authentication.password
|
285
|
+
}
|
286
|
+
|
287
|
+
urlString = urlString + '@'
|
288
|
+
}
|
289
|
+
|
290
|
+
urlString = urlString + uri.host.value
|
291
|
+
|
292
|
+
if (Objects.isPresent(uri.host.port)) {
|
293
|
+
urlString = urlString + ':' + uri.host.port
|
294
|
+
}
|
295
|
+
}
|
296
|
+
|
297
|
+
urlString = urlString + formatLocation(uri.location)
|
298
|
+
return urlString
|
299
|
+
}
|
300
|
+
|
301
|
+
const formatLocation = (location: UriLocation): string => {
|
302
|
+
let urlString = ''
|
303
|
+
|
304
|
+
if (!Strings.isEmpty(location.path)) {
|
305
|
+
urlString = urlString + location.path
|
306
|
+
}
|
307
|
+
|
308
|
+
if (!Strings.isEmpty(location.query)) {
|
309
|
+
urlString = urlString + '?' + location.query
|
310
|
+
}
|
311
|
+
|
312
|
+
if (!Strings.isEmpty(location.fragment)) {
|
313
|
+
urlString = urlString + '#' + encode(location.fragment!)
|
314
|
+
}
|
315
|
+
|
316
|
+
return urlString
|
317
|
+
}
|
318
|
+
|
319
|
+
export const buildString = (builder: UriBuilder): UriString => {
|
320
|
+
return format(build(builder))
|
321
|
+
}
|
package/src/url.ts
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
import { Arrays, Objects, Strings, Uris } from '@bessemer/cornerstone'
|
2
|
+
import { Dictionary } from '@bessemer/cornerstone/types'
|
3
|
+
import { Uri, UriBuilder, UriComponent, UriLocation, UriString } from '@bessemer/cornerstone/uri'
|
4
|
+
|
5
|
+
export const encode = Uris.encode
|
6
|
+
|
7
|
+
export const decode = Uris.decode
|
8
|
+
|
9
|
+
export interface UrlLocation extends UriLocation {
|
10
|
+
pathSegments: Array<string>
|
11
|
+
parameters: Dictionary<string | Array<string>>
|
12
|
+
}
|
13
|
+
|
14
|
+
export interface Url extends Uri {
|
15
|
+
location: UrlLocation
|
16
|
+
}
|
17
|
+
|
18
|
+
export type UrlLike = Url | UriString
|
19
|
+
|
20
|
+
const augmentUriLocation = (uriLocation: UriLocation, normalize: boolean): UrlLocation => {
|
21
|
+
const pathSegments: Array<string> = []
|
22
|
+
const parameters: Dictionary<string | Array<string>> = {}
|
23
|
+
|
24
|
+
if (!Strings.isBlank(uriLocation.path)) {
|
25
|
+
Strings.removeStart(uriLocation.path, '/')
|
26
|
+
.split('/')
|
27
|
+
.forEach((urlPathPart) => {
|
28
|
+
if (!Strings.isBlank(urlPathPart) || !normalize) {
|
29
|
+
pathSegments.push(decode(urlPathPart))
|
30
|
+
}
|
31
|
+
})
|
32
|
+
}
|
33
|
+
|
34
|
+
if (Objects.isPresent(uriLocation.query)) {
|
35
|
+
uriLocation.query.split('&').forEach((parameterPair) => {
|
36
|
+
let splitParameters = parameterPair.split('=')
|
37
|
+
|
38
|
+
if (!Strings.isBlank(Arrays.first(splitParameters))) {
|
39
|
+
let key = decode(splitParameters[0]!)
|
40
|
+
let value = ''
|
41
|
+
if (splitParameters.length === 2) {
|
42
|
+
value = splitParameters[1]!
|
43
|
+
}
|
44
|
+
if (Objects.isNil(parameters[key])) {
|
45
|
+
parameters[key] = decode(value)
|
46
|
+
} else if (!Array.isArray(parameters[key])) {
|
47
|
+
let paramList = [parameters[key]]
|
48
|
+
paramList.push(decode(value))
|
49
|
+
parameters[key] = paramList
|
50
|
+
} else {
|
51
|
+
parameters[key].push(decode(value))
|
52
|
+
}
|
53
|
+
}
|
54
|
+
})
|
55
|
+
}
|
56
|
+
|
57
|
+
return {
|
58
|
+
...uriLocation,
|
59
|
+
pathSegments,
|
60
|
+
parameters,
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
export const parse = (urlString: UriString, normalize: boolean = true): Url => {
|
65
|
+
const uri = Uris.parse(urlString)
|
66
|
+
const location = augmentUriLocation(uri.location, normalize)
|
67
|
+
|
68
|
+
if (normalize) {
|
69
|
+
if (!Arrays.isEmpty(location.pathSegments)) {
|
70
|
+
location.path = (Strings.startsWith(location.path, '/') ? '/' : '') + formatPathSegments(location.pathSegments)
|
71
|
+
} else {
|
72
|
+
location.path = ''
|
73
|
+
}
|
74
|
+
|
75
|
+
location.query = formatQueryParameters(location.parameters)
|
76
|
+
}
|
77
|
+
|
78
|
+
return {
|
79
|
+
...uri,
|
80
|
+
location,
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
export const format = Uris.format
|
85
|
+
|
86
|
+
const formatPathSegments = (pathSegments: Array<string>): UriComponent => {
|
87
|
+
return pathSegments.map((it) => encode(it)).join('/')
|
88
|
+
}
|
89
|
+
|
90
|
+
const formatQueryParameters = (parameters: Dictionary<string | Array<string>>): UriComponent | null => {
|
91
|
+
const parameterEntries = Object.entries(parameters)
|
92
|
+
if (Arrays.isEmpty(parameterEntries)) {
|
93
|
+
return null
|
94
|
+
}
|
95
|
+
|
96
|
+
return Object.entries(parameters)
|
97
|
+
.flatMap(([key, value]) => {
|
98
|
+
if (Array.isArray(value)) {
|
99
|
+
return value.map((it) => `${encode(key)}=${encode(it)}`)
|
100
|
+
} else {
|
101
|
+
return [`${encode(key)}=${encode(value)}`]
|
102
|
+
}
|
103
|
+
})
|
104
|
+
.join('&')
|
105
|
+
}
|
106
|
+
|
107
|
+
export type UrlBuilder = UriBuilder & {
|
108
|
+
location?: {
|
109
|
+
parameters?: Dictionary<string | Array<string>>
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
export const build = (builder: UrlBuilder): Url => {
|
114
|
+
const uri = Uris.build(builder)
|
115
|
+
if (Objects.isPresent(builder.location?.parameters)) {
|
116
|
+
uri.location.query = formatQueryParameters(builder.location.parameters)
|
117
|
+
}
|
118
|
+
|
119
|
+
const urlLocation = augmentUriLocation(uri.location, false)
|
120
|
+
|
121
|
+
return {
|
122
|
+
...uri,
|
123
|
+
location: urlLocation,
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
127
|
+
export const buildString = (builder: UrlBuilder): UriString => {
|
128
|
+
return format(build(builder))
|
129
|
+
}
|
130
|
+
|
131
|
+
export const reify = (blah: UrlLike): Url => {
|
132
|
+
if (!Strings.isString(blah)) {
|
133
|
+
return blah
|
134
|
+
}
|
135
|
+
|
136
|
+
return parse(blah)
|
137
|
+
}
|
138
|
+
|
139
|
+
export const getParameter = (url: UrlLike, name: string): string | undefined => {
|
140
|
+
const parameter = reify(url).location.parameters[name]
|
141
|
+
if (Objects.isNil(parameter)) {
|
142
|
+
return undefined
|
143
|
+
}
|
144
|
+
|
145
|
+
if (Array.isArray(parameter)) {
|
146
|
+
throw new Error(`Expected a single parameter value but found multiple for parameter: ${name}`)
|
147
|
+
}
|
148
|
+
|
149
|
+
return parameter
|
150
|
+
}
|
151
|
+
|
152
|
+
export const getJsonParameter = <T>(url: UrlLike, name: string): T | undefined => {
|
153
|
+
const value = getParameter(url, name)
|
154
|
+
return Objects.isPresent(value) ? JSON.parse(value) : undefined
|
155
|
+
}
|
package/src/uuid.ts
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
import { Objects, Preconditions, Strings } from '@bessemer/cornerstone'
|
2
|
+
import { TaggedType } from '@bessemer/cornerstone/types'
|
3
|
+
|
4
|
+
export type Uuid = TaggedType<string, 'Uuid'>
|
5
|
+
|
6
|
+
export const generate = (): Uuid => {
|
7
|
+
if (Objects.isNil(crypto.randomUUID)) {
|
8
|
+
return `${randomHex(8)}-${randomHex(4)}-${randomHex(4)}-${randomHex(4)}-${randomHex(12)}` as Uuid
|
9
|
+
} else {
|
10
|
+
return crypto.randomUUID() as Uuid
|
11
|
+
}
|
12
|
+
}
|
13
|
+
|
14
|
+
export const asString = (value: Uuid): string => {
|
15
|
+
return value
|
16
|
+
}
|
17
|
+
|
18
|
+
export const generateString = (): string => {
|
19
|
+
return asString(generate())
|
20
|
+
}
|
21
|
+
|
22
|
+
const randomHex = (characters: number) => {
|
23
|
+
// Generates a random number between 0x0..0 and 0xF..F for the target number of characters
|
24
|
+
const randomNum = Math.floor(Math.random() * (16 ** characters - 1))
|
25
|
+
return Strings.padStart(randomNum.toString(16), characters, '0')
|
26
|
+
}
|
27
|
+
|
28
|
+
export const of = (value: string): Uuid => {
|
29
|
+
Preconditions.isTrue(isValid(value), () => `Attempted to coerce invalid value to Uuid: ${value}`)
|
30
|
+
return value as Uuid
|
31
|
+
}
|
32
|
+
|
33
|
+
const pattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
34
|
+
|
35
|
+
export const isValid = (value: unknown): value is Uuid => {
|
36
|
+
return Strings.isString(value) && pattern.test(value)
|
37
|
+
}
|
package/src/zod.ts
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
import z, { ZodType } from 'zod'
|
2
|
+
import { ResourceKey } from '@bessemer/cornerstone/resource'
|
3
|
+
import { Entry } from '@bessemer/cornerstone/entry'
|
4
|
+
|
5
|
+
export type infer<T extends ZodType<any, any, any>> = z.infer<T>
|
6
|
+
export const object = z.object
|
7
|
+
export const string = z.string
|
8
|
+
export const union = z.union
|
9
|
+
export const array = z.array
|
10
|
+
export const unknown = z.unknown
|
11
|
+
export const nullType = z.null
|
12
|
+
export const undefined = z.undefined
|
13
|
+
|
14
|
+
export const arrayable = <T>(type: ZodType<T>) => {
|
15
|
+
return z.union([type, z.array(type)])
|
16
|
+
}
|
17
|
+
|
18
|
+
export const key = (): ZodType<ResourceKey> => {
|
19
|
+
return z.string()
|
20
|
+
}
|
21
|
+
|
22
|
+
export const entry = <Value, Key = string>(value: ZodType<Value>, key?: ZodType<Key>): ZodType<Entry<Value, Key>> => {
|
23
|
+
return z.tuple([key ?? z.string(), value]) as ZodType<Entry<Value, Key>>
|
24
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
test('TODO', () => {})
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import { Expressions, NumericExpressions, StringExpressions } from '@bessemer/cornerstone/expression'
|
2
|
+
|
3
|
+
test('TODO', () => {
|
4
|
+
NumericExpressions.sum([Expressions.variable('VitalityPoints'), 10])
|
5
|
+
|
6
|
+
Expressions.equals([NumericExpressions.sum([Expressions.variable('VitalityPoints'), 10]), 5])
|
7
|
+
|
8
|
+
Expressions.and([
|
9
|
+
NumericExpressions.lessThan(NumericExpressions.sum([Expressions.variable('VitalityPoints'), 10]), 15),
|
10
|
+
StringExpressions.substring('one', 'two'),
|
11
|
+
])
|
12
|
+
})
|