Skip to content

Commit

Permalink
feat: add better-serialize
Browse files Browse the repository at this point in the history
  • Loading branch information
RealShadowNova committed Jan 11, 2023
1 parent 966e10a commit dca5e54
Show file tree
Hide file tree
Showing 10 changed files with 1,204 additions and 8 deletions.
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export const packageName = 'better-serialize';
export * from './lib/parse';
export * from './lib/Serialize';
export * from './lib/stringify';
213 changes: 213 additions & 0 deletions src/lib/Serialize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { isObject } from './private';

/**
* The namespace which contains everything for the transformation of raw Node.js data types.
* @since 1.0.0
*/
export namespace Serialize {
/**
* Converts raw Node.js data types into a JSON-compatible format.
* @since 1.0.0
* @param data The raw Node.js data type to convert.
* @returns The JSON-compatible format of the raw Node.js data type.
*/
export function toJsonCompatible(data: unknown): JsonCompatible {
if (isObject(data)) return { [Keying.Type]: Type.Object, [Keying.Value]: toJsonCompatibleObject(data) };
else if (typeof data === 'string') return { [Keying.Type]: Type.String, [Keying.Value]: data };
else if (typeof data === 'number') {
if (Number.isNaN(data)) return { [Keying.Type]: Type.Number, [Keying.Value]: 'NaN' };
if ([Infinity, -Infinity].includes(data)) return { [Keying.Type]: Type.Number, [Keying.Value]: String(data) };

return { [Keying.Type]: Type.Number, [Keying.Value]: data };
} else if (typeof data === 'boolean') return { [Keying.Type]: Type.Boolean, [Keying.Value]: data };
else if (Array.isArray(data)) return { [Keying.Type]: Type.Array, [Keying.Value]: toJsonCompatibleArray(data) };
else if (data === null) return { [Keying.Type]: Type.Null };
else if (data instanceof Date) return { [Keying.Type]: Type.Date, [Keying.Value]: data.toJSON() };
else if (data === undefined) return { [Keying.Type]: Type.Undefined };
else if (typeof data === 'bigint') return { [Keying.Type]: Type.BigInt, [Keying.Value]: data.toString() };
else if (data instanceof RegExp) {
return { [Keying.Type]: Type.RegExp, [Keying.Value]: { [Keying.Source]: data.source, [Keying.Flags]: data.flags } };
} else if (data instanceof Map) return { [Keying.Type]: Type.Map, [Keying.Value]: toJsonCompatibleEntries(Array.from(data)) };
else if (data instanceof Set) return { [Keying.Type]: Type.Set, [Keying.Value]: toJsonCompatibleArray(Array.from(data)) };

throw new TypeError(`Serialize received an unknown type while formatting: "${data.constructor.name}".`);
}

function toJsonCompatibleArray(array: unknown[]): JsonCompatible[] {
const json: JsonCompatible[] = [];

for (const value of array) json.push(toJsonCompatible(value));

return json;
}

function toJsonCompatibleEntries(entries: [PropertyKey, unknown][]): [PropertyKey, JsonCompatible][] {
const json: [PropertyKey, JsonCompatible][] = [];

for (const [key, value] of entries) json.push([key, toJsonCompatible(value)]);
return json;
}

function toJsonCompatibleObject(object: Record<PropertyKey, unknown>): Record<PropertyKey, JsonCompatible> {
const json: Record<PropertyKey, JsonCompatible> = {};

for (const key in object) {
if (Object.prototype.hasOwnProperty.call(object, key)) {
json[key] = toJsonCompatible(object[key]);
}
}

return json;
}

/**
* Converts JSON-compatible data into a raw Node.js data format.
* @since 1.0.0
* @param json The JSON-compatible data to convert.
* @returns The raw Node.js data format of the JSON-compatible data.
*/
export function fromJsonCompatible(json: JsonCompatible): unknown {
const { [Keying.Type]: type, [Keying.Value]: value } = json;

switch (type) {
case Type.Array:
return fromJsonCompatibleArray(value as JsonCompatible[]);

case Type.BigInt:
return BigInt(value as string);

case Type.Boolean:
return value;

case Type.Date:
return new Date(value as string);

case Type.Map:
return new Map(fromJsonCompatibleMap(value as [PropertyKey, JsonCompatible][]));

case Type.Null:
return null;

case Type.Number:
if (typeof value === 'string') return Number(value);

return value;

case Type.Object:
return fromJsonCompatibleObject(value as Record<PropertyKey, JsonCompatible>);

case Type.RegExp:
return new RegExp((value as Regex)[Keying.Source], (value as Regex)[Keying.Flags]);

case Type.Set:
return new Set(fromJsonCompatibleArray(value as JsonCompatible[]));

case Type.String:
return value;

case Type.Undefined:
return undefined;

default:
throw new TypeError('Serialize received an unknown type.');
}
}

function fromJsonCompatibleArray(json: JsonCompatible[]): unknown[] {
const arr: unknown[] = [];

for (const value of json) arr.push(fromJsonCompatible(value));
return arr;
}

function fromJsonCompatibleMap(json: [PropertyKey, JsonCompatible][]): [PropertyKey, unknown][] {
const arr: [PropertyKey, unknown][] = [];

for (const [key, value] of json) arr.push([key, fromJsonCompatible(value)]);
return arr;
}

function fromJsonCompatibleObject(json: Record<PropertyKey, JsonCompatible>): Record<PropertyKey, unknown> {
const obj: Record<PropertyKey, unknown> = {};

for (const key in json) {
if (Object.prototype.hasOwnProperty.call(json, key)) {
obj[key] = fromJsonCompatible(json[key]);
}
}

return obj;
}

/**
* The keying used for JSON-compatible data.
* @since 1.0.0
*/
export enum Keying {
Type = 't',

Value = 'v',

Source = 's',

Flags = 'f'
}

/**
* The types of JSON-compatible data.
* @since 1.0.0
*/
export enum Type {
Array = 4,

BigInt = 8,

Boolean = 3,

Date = 6,

Map = 10,

Null = 5,

Number = 2,

Object = 0,

RegExp = 9,

Set = 11,

String = 1,

Undefined = 7
}

/**
* The JSON-compatible data format.
* @since 1.0.0
*/
export interface JsonCompatible {
[Keying.Type]: Type;

[Keying.Value]?:
| string
| number
| boolean
| null
| JsonCompatible[]
| [PropertyKey, JsonCompatible][]
| Record<PropertyKey, JsonCompatible>
| Regex;
}

/**
* The JSON-compatible RegExp format.
* @since 1.0.0
*/
export interface Regex {
[Keying.Source]: string;

[Keying.Flags]: string;
}
}
11 changes: 11 additions & 0 deletions src/lib/parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Serialize } from './Serialize';

/**
* Parses a {@link Serialize.JsonCompatible} string into it's raw Node.js data format.
* @since 1.0.0
* @param data The {@link Serialize.JsonCompatible} string to parse.
* @returns The raw Node.js data format of the {@link Serialize.JsonCompatible} string.
*/
export function parse(data: string): unknown {
return Serialize.fromJsonCompatible(JSON.parse(data));
}
1 change: 1 addition & 0 deletions src/lib/private/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../private/isObject';
3 changes: 3 additions & 0 deletions src/lib/private/isObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isObject<Value = unknown>(value: unknown): value is Record<PropertyKey, Value> {
return typeof value === 'object' && value?.constructor === Object;
}
11 changes: 11 additions & 0 deletions src/lib/stringify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Serialize } from './Serialize';

/**
* Stringifies a Node.js data format into a {@link Serialize.JsonCompatible} string.
* @since 1.0.0
* @param data The Node.js data format to stringify.
* @returns The {@link Serialize.JsonCompatible} string of the Node.js data format.
*/
export function stringify(data: unknown): string {
return JSON.stringify(Serialize.toJsonCompatible(data));
}
7 changes: 0 additions & 7 deletions tests/index.test.ts

This file was deleted.

Loading

0 comments on commit dca5e54

Please sign in to comment.