This repository was archived by the owner on Aug 30, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathinternals.ts
212 lines (183 loc) · 7.39 KB
/
internals.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import { TimeUnit } from "./typings"
export type Dictionary = { [key: string]: any }
/**
* @returns Whether the given `value` is a "dictionary object".
*/
export const isDictionary = (value: unknown): value is Dictionary =>
typeof value === "object"
&& value !== null
&& (value.toString() === "[object Object]")
&& !Array.isArray(value)
/**
* @returns Whether the given `value` is considered *falsy* by CertLogic.
* Note: the notions of both falsy and truthy are narrower than those of JavaScript, and even of JsonLogic.
* Truthy and falsy values can be used for conditional logic, e.g. the guard of an `if`-expression.
* Values that are neither truthy nor falsy (many of which exist) can't be used for that.
*/
export const isFalsy = (value: unknown) =>
value === false
|| value === null
|| (typeof value === "string" && value === "")
|| (typeof value === "number" && value === 0)
|| (Array.isArray(value) && value.length === 0)
|| (isDictionary(value) && Object.entries(value).length === 0)
/**
* @returns Whether the given `value` is considered *truthy* by CertLogic.
* @see isFalsy
*/
export const isTruthy = (value: unknown) =>
value === true
|| (typeof value === "string" && value !== "")
|| (typeof value === "number" && value !== 0)
|| (Array.isArray(value) && value.length > 0)
|| (isDictionary(value) && Object.entries(value).length > 0)
/**
* Type to encode truthy/falsy/neither (AKA “boolsiness”):
*
* * `true` ↔ truthy
* * `false` ↔ falsy
* * `undefined` ↔ neither
*/
export type Boolsiness = boolean | undefined
/**
* Determines boolsiness of the given JSON value.
*/
export const boolsiness = (value: unknown): Boolsiness => {
if (isTruthy(value)) {
return true
}
if (isFalsy(value)) {
return false
}
return undefined
}
/**
* @returns Whether the given value is an integer number.
*/
export const isInt = (value: unknown): value is number =>
typeof value === "number" && Number.isInteger(value)
/**
* A type for all CertLogic single-value literals.
*/
export type CertLogicLiteral = string | number | boolean
/**
* Determine whether the given value is a valid CertLogic literal expression,
* meaning: a string, an integer number, or a boolean.
*/
export const isCertLogicLiteral = (expr: any): expr is CertLogicLiteral =>
typeof expr === "string" || isInt(expr) || typeof expr === "boolean"
/**
* Named function to check whether something is a {@link Date}.
* @deprecated from 2.0.0 onwards (planned) - use `value instanceof Date` instead.
*/
export const isDate = (value: unknown): value is Date =>
value instanceof Date
const leftPad = (str: string, len: number, char: string): string => char.repeat(len - str.length) + str
const timeSuffix = "T00:00:00.000Z"
/**
* @returns A JavaScript {@see Date} object representing the date or date-time given as a string.
* @throws An {@see Error} in case the string couldn't be parsed as a date or date-time.
*/
export const dateFromString = (str: string) => {
if (str.match(/^\d{4}-\d{2}-\d{2}$/)) {
return new Date(`${str}${timeSuffix}`)
}
const matcher = str.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+?)?(Z|(([+-])(\d{1,2}):?(\d{2})?))?$/)
// 1 2 3 4 5 6 7 8 910 11 12
if (matcher) {
let reformatted = `${matcher[1]}-${matcher[2]}-${matcher[3]}T${matcher[4]}:${matcher[5]}:${matcher[6]}`
if (matcher[7]) {
reformatted += matcher[7].padEnd(4, "0").substring(0, 4)
}
if (!matcher[8] || (matcher[8] === "Z")) {
reformatted += "Z" // Assume timezone offset 'Z' when missing.
} else {
reformatted += matcher[10] + leftPad(matcher[11], 2, "0") + ":" + (matcher[12] || "00")
}
return new Date(reformatted)
}
throw new Error(`not an allowed date or date-time format: ${str}`)
}
/**
* @returns A {@link Date} that's the result of parsing a partial date `str` given in format `YYYY` or `YYYY-MM`,
* and “rounding that up” to (midnight +0 UTC of) the latest day consistent with that data.
*/
export const roundUpPartialDate = (str: string): Date | undefined => {
if (str.match(/^\d{4}$/)) {
return new Date(`${str}-12-31${timeSuffix}`)
}
if (str.match(/^\d{4}-\d{2}$/)) {
const date = new Date(`${str}-01${timeSuffix}`)
date.setUTCMonth(date.getUTCMonth() + 1)
date.setUTCDate(0)
return date
}
return undefined
}
/**
* @returns A {@link Date} that's the result of parsing `dateTimeLikeStr` as an ISO 8601 string using {@link dateFromString},
* with the indicated number of time units added to it.
* This treats partial dates in the format YYYY or YYYY-MM by “rounding up” to (midnight +0 UTC of) the latest day consistent
* with that data.
*/
export const plusTime = (dateTimeLikeStr: string, amount: number, unit: TimeUnit): Date => {
const dateTime = roundUpPartialDate(dateTimeLikeStr) ?? dateFromString(dateTimeLikeStr)
if (amount === 0) {
return dateTime
}
if (unit === "day") {
dateTime.setUTCDate(dateTime.getUTCDate() + amount)
} else if (unit === "hour") {
dateTime.setUTCHours(dateTime.getUTCHours() + amount)
} else if (unit === "month") {
dateTime.setUTCMonth(dateTime.getUTCMonth() + amount)
} else if (unit === "year") {
dateTime.setUTCFullYear(dateTime.getUTCFullYear() + amount)
} else {
throw new Error(`unknown time unit "${unit}"`)
}
return dateTime
}
/**
* @returns: A JavaScript {@see Date} representing the given date that may be partial (YYYY[-MM[-DD]]).
* See [the CertLogic specification](https://github.com/ehn-dcc-development/dgc-business-rules/blob/main/certlogic/specification/README.md) for details.
*/
export const dccDateOfBirth = (str: string): Date => {
const forPartialDate = roundUpPartialDate(str)
if (forPartialDate !== undefined) {
return forPartialDate
}
if (str.match(/^\d{4}-\d{2}-\d{2}$/)) {
return dateFromString(str)
}
throw new Error(`can't parse "${str}" as an EU DCC date-of-birth`)
}
const optionalPrefix = "URN:UVCI:"
/**
* @returns The fragment with given index from the UVCI string
* (see Annex 2 in the [UVCI specification](https://ec.europa.eu/health/sites/default/files/ehealth/docs/vaccination-proof_interoperability-guidelines_en.pdf)),
* or `null` when that fragment doesn't exist.
*/
export const extractFromUVCI = (uvci: string | null, index: number): string | null => {
if (uvci === null || index < 0) {
return null
}
const prefixlessUvci = uvci.startsWith(optionalPrefix) ? uvci.substring(optionalPrefix.length) : uvci
const fragments = prefixlessUvci.split(/[/#:]/)
return index < fragments.length ? fragments[index] : null
}
/**
* @returns The value found within `data` at the given `path`
* - this is the semantics of CertLogic's "var" operation.
*/
export const access = (data: any, path: string): any =>
path === "" // == "it"
? data
: path.split(".").reduce((acc, fragment) => {
if (acc === null) {
return null
}
const index = parseInt(fragment, 10)
const value = isNaN(index) ? acc[fragment] : acc[index]
return value === undefined ? null : value
}, data)