Skip to content

Commit af1b1bd

Browse files
committed
feat(parser): helpers to parse object to base64
1 parent 0606114 commit af1b1bd

6 files changed

Lines changed: 188 additions & 16 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@athenna/common",
3-
"version": "5.31.0",
3+
"version": "5.32.0",
44
"description": "The Athenna common helpers to use in any Node.js ESM project.",
55
"license": "MIT",
66
"author": "João Lenon <lenon@athenna.io>",

src/handlers/ExceptionHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type ExceptionHandlerContext = {
1616
export class ExceptionHandler extends Macroable {
1717
public async handle(_: ExceptionHandlerContext): Promise<void> {
1818
/**
19-
* This method is meant to be overridden by the user
19+
* This method is meant to be overridden by the user
2020
* using the `macro()` method
2121
*/
2222
}

src/helpers/Enum.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ export class Enum extends Macroable {
7272
* public static PENDING = 'pending' as const
7373
* public static APPROVED = 'approved' as const
7474
* public static BLOCKED = 'blocked' as const
75-
* }
76-
*
75+
* }
76+
*
7777
* const randomKey = StatusEnum.randomKey() // 'PENDING'
7878
* ```
7979
*/
@@ -90,8 +90,8 @@ export class Enum extends Macroable {
9090
* public static PENDING = 'pending' as const
9191
* public static APPROVED = 'approved' as const
9292
* public static BLOCKED = 'blocked' as const
93-
* }
94-
*
93+
* }
94+
*
9595
* const randomValue = StatusEnum.randomValue() // 'pending'
9696
* ```
9797
*/

src/helpers/Parser.ts

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,16 @@ import bytes from 'bytes'
1212
import yaml from 'js-yaml'
1313
import csvParser from 'csv-parser'
1414

15+
import {
16+
json2csv,
17+
csv2json,
18+
type Json2CsvOptions,
19+
type Csv2JsonOptions
20+
} from 'json-2-csv'
21+
import type { HTMLJson } from '#src/types/json/HTMLJson'
22+
1523
import { Is } from '#src/helpers/Is'
24+
import { Clean } from '#src/helpers/Clean'
1625
import { String } from '#src/helpers/String'
1726
import { Options } from '#src/helpers/Options'
1827
import { Macroable } from '#src/helpers/Macroable'
@@ -21,14 +30,6 @@ import type { ObjectBuilderOptions } from '#src/types'
2130
import { getReasonPhrase, getStatusCode } from 'http-status-codes'
2231
import { InvalidNumberException } from '#src/exceptions/InvalidNumberException'
2332

24-
import {
25-
json2csv,
26-
csv2json,
27-
type Json2CsvOptions,
28-
type Csv2JsonOptions
29-
} from 'json-2-csv'
30-
import type { HTMLJson } from '#src/types/json/HTMLJson'
31-
3233
export class Parser extends Macroable {
3334
/**
3435
* Parse using Node.js streams, useful for
@@ -216,6 +217,68 @@ export class Parser extends Macroable {
216217
return bytes.parse(byte)
217218
}
218219

220+
/**
221+
* Parses an object to a base64 string.
222+
*/
223+
public static objectToBase64(
224+
object: Record<string, any>,
225+
options?: { nullOnEmpty?: boolean; removeEmpty?: boolean }
226+
) {
227+
options = Options.create(options, {
228+
nullOnEmpty: false,
229+
removeEmpty: false
230+
})
231+
232+
if (options.removeEmpty) {
233+
object = Clean.cleanObject(object, { removeEmpty: true, recursive: true })
234+
}
235+
236+
if (options.nullOnEmpty && Is.Empty(object)) {
237+
return null
238+
}
239+
240+
return Buffer.from(JSON.stringify(object)).toString('base64')
241+
}
242+
243+
/**
244+
* Parses a base64 string to an object.
245+
*/
246+
public static base64ToObject<T = Record<string, any>>(base64: string): T {
247+
return JSON.parse(Buffer.from(base64, 'base64').toString('utf-8'))
248+
}
249+
250+
/**
251+
* Parses an object to a base64url string.
252+
*/
253+
public static objectToBase64Url(
254+
object: Record<string, any>,
255+
options?: { nullOnEmpty?: boolean; removeEmpty?: boolean }
256+
) {
257+
options = Options.create(options, {
258+
nullOnEmpty: false,
259+
removeEmpty: false
260+
})
261+
262+
if (options.removeEmpty) {
263+
object = Clean.cleanObject(object, { removeEmpty: true, recursive: true })
264+
}
265+
266+
if (options.nullOnEmpty && Is.Empty(object)) {
267+
return null
268+
}
269+
270+
return Buffer.from(JSON.stringify(object)).toString('base64url')
271+
}
272+
273+
/**
274+
* Parses a base64url string to an object.
275+
*/
276+
public static base64UrlToObject<T = Record<string, any>>(
277+
base64Url: string
278+
): T {
279+
return JSON.parse(Buffer.from(base64Url, 'base64url').toString('utf-8'))
280+
}
281+
219282
/**
220283
* Parses a string to MS format.
221284
*/
@@ -230,6 +293,20 @@ export class Parser extends Macroable {
230293
return ms(value, { long })
231294
}
232295

296+
/**
297+
* Parses a time string to seconds format.
298+
*/
299+
public static timeToSeconds(value: string): number {
300+
return ms(value) / 1000
301+
}
302+
303+
/**
304+
* Parses a seconds number to time format.
305+
*/
306+
public static secondsToTime(value: number, long = false): string {
307+
return ms(value * 1000, { long })
308+
}
309+
233310
/**
234311
* Parses a json to a csv string.
235312
*/

tests/unit/helpers/ParserTest.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,25 @@ export default class ParserTest {
119119
assert.equal(Parser.msToTime(-31557600000, true), '-365 days')
120120
}
121121

122+
@Test()
123+
public async shouldParseTheStringToMsFormatAndSecondsFormatToString({ assert }: Context) {
124+
// time to seconds
125+
assert.equal(Parser.timeToSeconds('2 days'), 172800)
126+
assert.equal(Parser.timeToSeconds('1d'), 86400)
127+
assert.equal(Parser.timeToSeconds('10h'), 36000)
128+
assert.equal(Parser.timeToSeconds('-10h'), -36000)
129+
assert.equal(Parser.timeToSeconds('1 year'), 31557600)
130+
assert.equal(Parser.timeToSeconds('-1 year'), -31557600)
131+
132+
// ms to time
133+
assert.equal(Parser.secondsToTime(172800), '2 days')
134+
assert.equal(Parser.secondsToTime(86400), '1d')
135+
assert.equal(Parser.secondsToTime(36000), '10h')
136+
assert.equal(Parser.secondsToTime(-36000), '-10h')
137+
assert.equal(Parser.secondsToTime(31557600), '365 days')
138+
assert.equal(Parser.secondsToTime(-31557600), '-365 days')
139+
}
140+
122141
@Test()
123142
public async shouldParseTheStatusCodeToReasonAndReasonToStatusCode({ assert }: Context) {
124143
// status code to reason
@@ -280,6 +299,82 @@ export default class ParserTest {
280299
assert.deepEqual(object, { version: 3 })
281300
}
282301

302+
@Test()
303+
public async shouldBeAbleToParseAnObjectToBase64String({ assert }: Context) {
304+
const string1 = Parser.objectToBase64({ hello: 'world' })
305+
const string2 = Parser.objectToBase64(
306+
{
307+
hello: 'world',
308+
foo: null,
309+
bar: undefined,
310+
baz: {},
311+
qux: [],
312+
quux: ''
313+
},
314+
{ removeEmpty: true }
315+
)
316+
const string3 = Parser.objectToBase64(
317+
{
318+
foo: null,
319+
bar: undefined,
320+
baz: {},
321+
qux: []
322+
},
323+
{ nullOnEmpty: true, removeEmpty: true }
324+
)
325+
326+
assert.deepEqual(string1, 'eyJoZWxsbyI6IndvcmxkIn0=')
327+
assert.deepEqual(string2, 'eyJoZWxsbyI6IndvcmxkIn0=')
328+
assert.deepEqual(string3, null)
329+
}
330+
331+
@Test()
332+
public async shouldBeAbleToParseBase64StringToObject({ assert }: Context) {
333+
const string = 'eyJoZWxsbyI6IndvcmxkIn0='
334+
335+
const object = Parser.base64ToObject(string)
336+
337+
assert.deepEqual(object, { hello: 'world' })
338+
}
339+
340+
@Test()
341+
public async shouldBeAbleToParseAnObjectToBase64UrlString({ assert }: Context) {
342+
const string1 = Parser.objectToBase64Url({ hello: 'world' })
343+
const string2 = Parser.objectToBase64Url(
344+
{
345+
hello: 'world',
346+
foo: null,
347+
bar: undefined,
348+
baz: {},
349+
qux: [],
350+
quux: ''
351+
},
352+
{ removeEmpty: true }
353+
)
354+
const string3 = Parser.objectToBase64Url(
355+
{
356+
foo: null,
357+
bar: undefined,
358+
baz: {},
359+
qux: []
360+
},
361+
{ nullOnEmpty: true, removeEmpty: true }
362+
)
363+
364+
assert.deepEqual(string1, 'eyJoZWxsbyI6IndvcmxkIn0')
365+
assert.deepEqual(string2, 'eyJoZWxsbyI6IndvcmxkIn0')
366+
assert.deepEqual(string3, null)
367+
}
368+
369+
@Test()
370+
public async shouldBeAbleToParseBase64UrlStringToObject({ assert }: Context) {
371+
const string = 'eyJoZWxsbyI6IndvcmxkIn0'
372+
373+
const object = Parser.base64UrlToObject(string)
374+
375+
assert.deepEqual(object, { hello: 'world' })
376+
}
377+
283378
@Test()
284379
public async shouldBeAbleToParseArrayToCsv({ assert }: Context) {
285380
const csv = Parser.arrayToCsv([{ id: 1, name: 'lenon' }])

0 commit comments

Comments
 (0)