@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.
Files changed (66) hide show
  1. package/jest.config.js +3 -0
  2. package/package.json +39 -0
  3. package/src/array.ts +142 -0
  4. package/src/async.ts +114 -0
  5. package/src/cache.ts +236 -0
  6. package/src/combinable.ts +40 -0
  7. package/src/comparator.ts +78 -0
  8. package/src/content.ts +138 -0
  9. package/src/context.ts +6 -0
  10. package/src/crypto.ts +11 -0
  11. package/src/date.ts +18 -0
  12. package/src/duration.ts +57 -0
  13. package/src/either.ts +29 -0
  14. package/src/entry.ts +21 -0
  15. package/src/equalitor.ts +12 -0
  16. package/src/error-event.ts +126 -0
  17. package/src/error.ts +16 -0
  18. package/src/expression/array-expression.ts +29 -0
  19. package/src/expression/expression-evaluator.ts +34 -0
  20. package/src/expression/expression.ts +188 -0
  21. package/src/expression/internal.ts +34 -0
  22. package/src/expression/numeric-expression.ts +182 -0
  23. package/src/expression/string-expression.ts +38 -0
  24. package/src/expression.ts +48 -0
  25. package/src/function.ts +3 -0
  26. package/src/glob.ts +19 -0
  27. package/src/global-variable.ts +40 -0
  28. package/src/hash.ts +28 -0
  29. package/src/hex-code.ts +6 -0
  30. package/src/index.ts +82 -0
  31. package/src/lazy.ts +11 -0
  32. package/src/logger.ts +144 -0
  33. package/src/math.ts +132 -0
  34. package/src/misc.ts +22 -0
  35. package/src/object.ts +236 -0
  36. package/src/patch.ts +128 -0
  37. package/src/precondition.ts +25 -0
  38. package/src/promise.ts +16 -0
  39. package/src/property.ts +29 -0
  40. package/src/reference.ts +68 -0
  41. package/src/resource.ts +32 -0
  42. package/src/result.ts +66 -0
  43. package/src/retry.ts +70 -0
  44. package/src/rich-text.ts +24 -0
  45. package/src/set.ts +46 -0
  46. package/src/signature.ts +20 -0
  47. package/src/store.ts +91 -0
  48. package/src/string.ts +173 -0
  49. package/src/tag.ts +68 -0
  50. package/src/types.ts +21 -0
  51. package/src/ulid.ts +28 -0
  52. package/src/unit.ts +4 -0
  53. package/src/uri.ts +321 -0
  54. package/src/url.ts +155 -0
  55. package/src/uuid.ts +37 -0
  56. package/src/zod.ts +24 -0
  57. package/test/comparator.test.ts +1 -0
  58. package/test/expression.test.ts +12 -0
  59. package/test/object.test.ts +104 -0
  60. package/test/patch.test.ts +170 -0
  61. package/test/set.test.ts +20 -0
  62. package/test/string.test.ts +22 -0
  63. package/test/uri.test.ts +111 -0
  64. package/test/url.test.ts +174 -0
  65. package/tsconfig.build.json +13 -0
  66. 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
@@ -0,0 +1,4 @@
1
+ import { NominalType } from '@bessemer/cornerstone/types'
2
+
3
+ export type Unit = NominalType<null, 'Unit'>
4
+ export const Unit: Unit = null as never
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
+ })