/
json.ts
157 lines (131 loc) · 3.92 KB
/
json.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
import {
ResultKind,
anyCharOf,
anyStringOf,
float,
noCharOf,
string,
stringLen,
whitespace
} from "../lib";
import {
between,
later,
many,
manySepBy,
map,
or,
qthen,
stringify,
then,
thenq
} from "../lib/combinators";
import { visualizeTrace } from "../lib/internal/trace-visualizer";
export class JsonNumber {
constructor(public value: number) {}
}
export class JsonString {
constructor(public value: string) {}
}
export class JsonBool {
constructor(public value: boolean) {}
}
export class JsonArray {
constructor(public value: JsonValue[]) {}
}
export class JsonObjectProperty {
constructor(
public name: string,
public value: JsonValue
) {}
}
export class JsonObject {
constructor(public value: JsonObjectProperty[]) {}
}
export type JsonValue = JsonNumber | JsonString | JsonBool | JsonArray | JsonObject;
const escapes: Record<string, string> = {
'"': `"`,
"\\": "\\",
"/": "/",
f: "\f",
n: "\n",
r: "\r",
t: "\t"
};
export const pJsonValue = later<JsonValue>();
const pEscapeChar = anyCharOf(Object.getOwnPropertyNames(escapes).join()).pipe(
map(char => escapes[char] as string)
);
// A unicode escape sequence is "u" followed by exactly 4 hex digits
const pEscapeUnicode = string("u").pipe(
qthen(
stringLen(4).pipe(
map(str => parseInt(str, 16)),
map(x => String.fromCharCode(x))
)
)
);
// Any escape sequence begins with a \
const pEscapeAny = string("\\").pipe(qthen(pEscapeChar.pipe(or(pEscapeUnicode))));
// Here we process regular characters vs escape sequences
const pCharOrEscape = pEscapeAny.pipe(or(noCharOf('"')));
// Repeat the char/escape to get a sequence, and then put between quotes to get a string
const pStr = pCharOrEscape.pipe(many(), stringify(), between('"'));
// This is also a JSON string value
const pJsonString = pStr.pipe(map(str => new JsonString(str)));
// Parjs has a dedicated floating point number parser
const pNumber = float().pipe(map(n => new JsonNumber(n)));
// Parse bools
const pBool = anyStringOf("true", "false").pipe(map(str => new JsonBool(str === "true")));
// An array is a sequence of JSON values between ,s
const pArray = pJsonValue.pipe(
manySepBy(","),
between("[", "]"),
map(arr => new JsonArray(arr))
);
// An object property is a string key, and then a value, separated by : and whitespace around it
// Plus, whitespace around the property
const pObjectProperty = pStr.pipe(
thenq(string(":").pipe(between(whitespace()))),
then(pJsonValue),
between(whitespace()),
map(([name, value]) => {
return new JsonObjectProperty(name, value);
})
);
// An object is a sequence of object properties between {...} separated by ","
const pObject = pObjectProperty.pipe(
manySepBy(","),
between("{", "}"),
map(properties => new JsonObject(properties))
);
pJsonValue.init(pJsonString.pipe(or(pNumber, pBool, pArray, pObject), between(whitespace())));
function astToObject(obj: JsonValue): unknown {
if (obj instanceof JsonNumber) {
return obj.value;
} else if (obj instanceof JsonString) {
return obj.value;
} else if (obj instanceof JsonBool) {
return obj.value;
} else if (obj instanceof JsonArray) {
return obj.value.map(x => astToObject(x));
} else if (obj instanceof JsonObject) {
const res: Record<string, unknown> = {};
for (const prop of obj.value) {
res[prop.name] = astToObject(prop.value);
}
return res;
}
}
export const exampleInput = `{"a" : 2,
"b\\"" :
44325, "z" : "hi!", "a" : true,
"array" : ["hi", 1, {"a" : "b\\"" }, [], {}]}`;
export function runExample() {
const result = pJsonValue.parse(exampleInput);
if (result.kind !== ResultKind.Ok) {
console.log(visualizeTrace(result.trace));
} else {
console.log(astToObject(result.value));
}
}