tjson-js
Version:
Tagged JSON (TJSON): a JSON-based microformat with rich type annotations
161 lines (136 loc) • 4.85 kB
text/typescript
// TJSON: Tagged JSON with Rich Types
import * as utf8 from "utf8";
import { DataType } from "./datatype";
import { ArrayType } from "./datatype/array";
import { SetType } from "./datatype/set";
import { Binary16Type, Binary32Type, Binary64Type } from "./datatype/binary";
import { BooleanType } from "./datatype/boolean";
import { FloatType } from "./datatype/float";
import { IntType, UintType } from "./datatype/integer";
import { ObjectType } from "./datatype/object";
import { StringType } from "./datatype/string";
import { TimestampType } from "./datatype/timestamp";
DataType.register(new Binary16Type);
DataType.register(new Binary32Type);
DataType.register(new Binary64Type);
DataType.register(new Binary64Type, "d64");
DataType.register(new BooleanType);
DataType.register(new FloatType);
DataType.register(new IntType);
DataType.register(new UintType);
DataType.register(new ObjectType);
DataType.register(new StringType);
DataType.register(new TimestampType);
export default class TJSON {
// Parse a UTF-8 encoded TJSON string
public static parse(tjsonString: string, decodeUTF8 = true) {
let decodedString = decodeUTF8 ? utf8.decode(tjsonString) : tjsonString;
let result = JSON.parse(decodedString, TJSON.reviver);
if (Array.isArray(result)) {
throw new Error("toplevel arrays are not allowed in TJSON");
}
return result;
}
// Convert a value to TJSON
public static stringify(value: any, space: number | string = 0, encodeUTF8 = true): string {
let result = JSON.stringify(value, TJSON.replacer, space);
return encodeUTF8 ? utf8.encode(result) : result;
}
// Parse a TJSON type signature into the corresponding DataType object
public static parseType(tag: string): DataType {
// Object
if (tag == "O") {
return DataType.get("O");
}
// Non-Scalar
let result = tag.match(/^([A-Z][a-z0-9]*)<(.*)>$/);
if (result !== null) {
let prefix = result[1];
let inner = result[2];
switch (prefix) {
case "A":
if (inner == "") {
return new ArrayType(null);
} else {
return new ArrayType(TJSON.parseType(inner));
}
case "S":
if (inner == "") {
return new SetType(null);
} else {
return new SetType(TJSON.parseType(inner));
}
case "O":
return DataType.get(tag);
default:
throw new Error(`invalid non-scalar type: ${tag}`);
}
}
// Scalar
if (/^[a-z][a-z0-9]*$/.test(tag)) {
return DataType.get(tag);
} else {
throw new Error(`invalid tag: "${tag}"`);
}
}
// Identify the TJSON DataType for a given value
public static identifyType(value: any): DataType {
if (typeof value === "object") {
if (Array.isArray(value)) {
return ArrayType.identifyType(value);
} else if (value instanceof Uint8Array) {
return DataType.get("d");
} else if (value instanceof Set) {
return SetType.identifyType(value);
} else if (value instanceof Date) {
return DataType.get("t");
} else {
return DataType.get("O");
}
} else if (typeof value === "string") {
return DataType.get("s");
} else if (typeof value === "number") {
return DataType.get("f");
} else if (typeof value === "boolean") {
return DataType.get("b");
} else {
throw new Error(`unsupported TJSON value: ${value}`);
}
}
// Look for objects in parsed JSON, extract their types, and decode their values
static reviver(_key: any, value: any): any {
// Ignore non-objects (including arrays). Transform them on the next pass
if (typeof value !== "object" || Array.isArray(value)) {
return value;
}
let result: any = {};
Object.keys(value).forEach(key => {
let tagResult = key.match(/^(.*):([A-Za-z0-9\<]+[\>]*)$/);
if (tagResult === null) {
throw new Error(`failed to parse tag: ${key}`);
}
let untaggedKey = tagResult[1];
let tag = tagResult[2];
let childValue = value[key];
let type = TJSON.parseType(tag);
result[untaggedKey] = type.decode(childValue);
});
return result;
}
// Serializer for TJSON objects when building JSON strings
static replacer(_key: any, value: any): any {
// Ignore non-objects (including arrays). They'll already by transformed
// at this point (by the code below)
if (typeof value !== "object" || Array.isArray(value)) {
return value;
}
let result: any = {};
Object.keys(value).forEach(key => {
let childValue = value[key];
let type = TJSON.identifyType(childValue);
// Add tag to resulting field
result[key + ":" + type.tag()] = type.encode(childValue);
});
return result;
}
}