Note:
If you want this type of parser added for JSON, I think WRI could do it easily. They already have Developer`ReadRawJSONStream
so I think adding some event-based parsing to that wouldn't be too much to ask
This has been kicking around my for the past few days, so here's a quick example of what I discussed in my comment. It is by no means complete, but it's a start.
We'll simply define a bunch of different tokens to look for and handle each of them differently and then layer a reader on top of that. Here's the core code for that:
jStreamReader[stream : _String | _File | _InputStream] :=
Module[{
jStreamReader,
jStreamData
},
jStreamReader~ClearAttributes~Temporary;
jStreamData~ClearAttributes~Temporary;
jStreamData =
<|
"InString" -> False,
"BlockDepth" -> <|Association -> 0, List -> 0,
CompoundExpression -> 0|>,
"BlockStack" -> {},
"Cache" -> Internal`Bag[],
"CacheStack" -> {},
"CachePush" ->
Function[
Internal`StuffBag[jStreamData["Cache"], #]
],
"CacheRecurse" ->
Function[
jStreamData["CacheStack"] = {jStreamData["CacheStack"],
jStreamData["Cache"]};
jStreamData["Cache"] = Internal`Bag[];
],
"CacheReset" ->
Function[
With[{c = jStreamData["CacheStack"]},
jStreamData["CacheStack"] = c[[1]];
jStreamData["Cache"] = c[[2]];
]
],
"CachePart" ->
Function[
Internal`BagPart[jStreamData["Cache"], #]
],
"CacheClear" ->
Function[
jStreamData["Cache"] = Internal`Bag[];
],
"CacheDump" ->
Function[
With[{data = Internal`BagPart[jStreamData["Cache"], All]},
jStreamData["Cache"] = Internal`Bag[];
data
]
],
"Stream" ->
Replace[stream, {
_File | _String?FileExistsQ :> OpenRead[stream],
_String :> StringToStream[stream]
}]
|>;
jStreamReader["HeldSymbol"] = Hold[jStreamData];
jStreamReader["Symbol"] := Unevaluated[jStreamData];
jStreamReader["Data"] := jStreamData;
jStreamReader["Next"] := jStreamReadToken[jStreamReader];
jStreamReader[s__] := jStreamData[s];
jStreamReader /: Set[jStreamReader[p___], v_] :=
jStreamData[p] = v;
jStreamReader /: ReadString[jStreamReader, a___] :=
ReadString[jStreamData["Stream"], a];
jStreamReader /: Read[jStreamReader, a___] :=
Read[jStreamData["Stream"], a];
jStreamReader /: ReadList[jStreamReader, a___] :=
ReadList[jStreamData["Stream"], a];
jStreamReader /: StreamPosition[jStreamReader] :=
StreamPosition@jStreamData["Stream"];
jStreamReader /: SetStreamPosition[jStreamReader, p_] :=
SetStreamPosition[jStreamData["Stream"], p];
jStreamReader /: Close[jStreamReader] :=
(
Close@jStreamData["Stream"];
jStreamData // Remove
);
jStreamReader
]
$jStreamBracketTokenIndicators =
{"{", "}", "[", "]", "(", ")"};
$jStreamChunkTokenIndicators =
{",", ":"};
$jStreamStringTokenIndicators =
{"\""};
$jStreamInStringTokenIndicators =
{"\\\"", "\""};
$jStreamPossibleTokenIndicators =
Alternatives @@ Join[
$jStreamStringTokenIndicators,
$jStreamBracketTokenIndicators,
$jStreamChunkTokenIndicators
];
$jStreamInStringTokenIndicators =
Alternatives @@ $jStreamInStringTokenIndicators;
$jStreamBracketTokenIndicators =
Alternatives @@ $jStreamBracketTokenIndicators;
$jStreamBracketMap = <|
"{" -> {Opening, Association},
"}" -> {Closing, Association},
"[" -> {Opening, List},
"]" -> {Closing, List},
"(" -> {Opening, CompoundExpression},
")" -> {Closing, CompoundExpression}
|>;
jStreamReadToSep[reader_] :=
Module[{
sp = StreamPosition@reader,
strm = reader["Stream"],
ret,
sep
},
ret =
With[{test = Read[strm, Character]},
If[test === EndOfFile, Throw[EndOfFile]];
SetStreamPosition[strm, sp];
If[reader["InString"],
If[StringMatchQ[test, $jStreamInStringTokenIndicators] ||
StringMatchQ[
test <> "\"", $jStreamInStringTokenIndicators],
"",
ReadString[strm, $jStreamInStringTokenIndicators]
],
If[StringMatchQ[test, $jStreamPossibleTokenIndicators],
"",
ReadString[strm, $jStreamPossibleTokenIndicators]
]
]
];
sep =
If[reader["InString"],
With[{ec = Read[reader, Character]},
If[ec === "\\", ec <> Read[reader, Character], ec]
],
Read[reader, Character]
];
If[StringMatchQ[sep, $jStreamBracketTokenIndicators] &&
StringLength@StringTrim[ret] > 0,
SetStreamPosition[reader, StreamPosition[reader] - 1];
sep = ""
];
{ret, sep, sp}
];
jStreamHandleSepData[reader_, {ret_, sep_, sp_}] :=
With[{sepData = $jStreamBracketMap[sep]},
Which[
sep === "",
{ret, sep, Value, Expression},
sep === ":",
{ret, sep, Key, None},
sep === ",",
{ret, sep, Value, Expression},
sep === "\"" && reader["InString"],
reader["InString"] = False;
{ret, sep, None, String},
sep === "\"",
reader["InString"] = True;
{ret, sep, None, Continue},
sep === "\\\"",
{ret, sep, None, Continue},
sepData[[1]] === Closing,
If[reader["BlockStack"][[-1]] == sepData[[2]],
reader["BlockDepth", sepData[[2]]] =
Max@{reader["BlockDepth", sepData[[2]]] - 1, 0};
reader["BlockStack"] = Drop[reader["BlockStack"], -1]
];
{ret, sep, sepData, Expression},
sepData[[1]] === Opening,
reader["BlockDepth", sepData[[2]]] =
Max@{reader["BlockDepth", sepData[[2]]] - 1, 0};
reader["BlockStack"] =
Append[reader["BlockStack"], sepData[[2]]];
{ret, sep, sepData, None},
True,
{ret, sep, sepData, Expression}
]
];
jStreamReadTokenSegments[reader_] :=
Block[{segs},
segs = {None,
jStreamHandleSepData[reader, jStreamReadToSep[reader]]};
While[segs[[-1, -1]] === Continue,
segs = {segs,
jStreamHandleSepData[reader, jStreamReadToSep[reader]]}
];
Cases[segs, {_String, _String, __}, \[Infinity]]
]
jStreamBuildToken[segs_] :=
{
Switch[segs[[-1, -1]],
None, Null,
String,
{
ToExpression@StringJoin@segs[[All, ;; 2]],
String
},
_,
{
Replace[StringTrim@StringJoin@segs[[-1, 1]], {
"null" -> Null,
e_ :> ToExpression@e
}],
segs[[-1, -1]]
}
],
Replace[segs[[-1, -2]], {
{o_, "{}"} :> {o, Association},
{o_, "[]"} :> {o, List},
{o_, "()"} :> CompoundExpression
}]
}
jStreamReadToken[reader_] :=
Catch@jStreamBuildToken@jStreamReadTokenSegments[reader];
This generates a reader symbol that tracks the data in a mutable way, allowing it to act like a prettied-up stream in many ways.
Here's support for reading JSON data types:
(*Different types of read processors *)
jStreamRead[reader_, Key] :=
Replace[jStreamReadToken[reader], {
{Null, Key} :>
Replace[reader["CacheDump"][][[-1, 1]],
{a_, String | Expression} :> a
],
EndOfFile :> EndOfFile,
e_ :>
(
reader["CachePush"][e];
jStreamRead[reader, Key]
)
}];
jStreamRead[reader_, Association] :=
Replace[jStreamReadToken[reader], {
{_, {Closing, Association}} :>
Association@Cases[reader["CacheDump"][], _Rule],
{Null, Key} :>
(
reader["CachePush"][
reader["CachePart"][-1][[1, 1]] ->
(
reader["CacheRecurse"][];
((reader["CacheReset"][]; #) &@jStreamRead[reader, Value])
)
];
jStreamRead[reader, Association]
),
EndOfFile :> EndOfFile,
e_ :>
(
reader["CachePush"][e];
jStreamRead[reader, Association]
)
}];
jStreamRead[reader_, List] :=
Replace[jStreamReadToken[reader], {
{_, {Closing, List}} :>
reader["CacheDump"][],
{{v_, _}, None | Value} :>
(
reader["CachePush"][v];
jStreamRead[reader, CompoundExpression]
),
{Null, {Opening, t_}} :>
(
reader["CachePush"][
jStreamRead[reader, t]
];
jStreamRead[reader, CompoundExpression]
),
EndOfFile :> EndOfFile
}];
jStreamRead[reader_, CompoundExpression] :=
Replace[jStreamReadToken[reader], {
{_, {Closing, CompoundExpression}} :>
$wrapper @@
reader["CacheDump"],
{Null, {Opening, t_}} :> jStreamRead[reader, t],
{{v_, _}, None | Value} :>
(
reader["CachePush"][v];
jStreamRead[reader, CompoundExpression]
),
{Null, {Opening, t_}} :>
(
reader["CachePush"][
jStreamRead[reader, t]
];
jStreamRead[reader, CompoundExpression]
),
EndOfFile :> EndOfFile
}];
jStreamRead[reader_, Value] :=
Replace[jStreamReadToken[reader], {
{{a_, _}, None | Value} :>
a,
{Null, {Opening, t_}} :> jStreamRead[reader, t],
EndOfFile :> EndOfFile,
e_ :>
(
reader["CacheAdd"][e];
jStreamRead[reader, Value]
)
}];
jStreamRead[reader_, KeyValuePattern] :=
jStreamRead[reader, Key] ->
jStreamRead[reader, Value];
jStreamRead[reader_, Automatic] :=
Replace[jStreamReadToken[reader], {
{Null, {Opening, t_}} :> jStreamRead[reader, t],
{{a_, _}, None | Value} :>
a
}];
jStreamRead[reader_, "Token"] :=
jStreamReadToken[reader];
We'll then layer a more convenient interface on top of that:
JSONStream[strm : _String | _File | _InputStream] :=
JSONStream[jStreamReader[strm]];
JSONStreamRead[JSONStream[s_Symbol], type_: "Token"] :=
Replace[jStreamRead[s, type],
EndOfFile | (_ -> EndOfFile) :> JSONStreamClose[JSONStream[s]]
];
JSONStreamClose[JSONStream[s_Symbol]] :=
(
Close@s;
Remove[s];
EndOfFile
);
JSONStream /: Read[JSONStream[s_Symbol], type___] :=
JSONStreamRead[JSONStream[s], type];
JSONStream /: Close[JSONStream[s_Symbol]] :=
JSONStreamClose[JSONStream[s]];
Format[JSONStream[
s_Symbol?(MatchQ[#["Data"]["Stream"], _InputStream] &)]] :=
With[{is = s["Data"]["Stream"]},
RawBoxes@
BoxForm`ArrangeSummaryBox[
"JSONStream",
JSONStream[s],
BoxForm`GenericIcon[InputStream],
{
BoxForm`SummaryItem[{"Name: ",
Replace[is[[1]], f_String :> FileNameTake@f]}],
BoxForm`SummaryItem[{"Unique ID: ", is[[2]]}]
},
{
BoxForm`SummaryItem[{"Open: ",
Dynamic[Options[is] =!= {},
UpdateInterval -> 1
]
}]
},
StandardForm
]
]
Finally generate a JSON sample and we can start reading tokens:
$jsSample =
"\n" <>
ExportString[
Prepend[ExampleData[{"Dataset", "Planets"}],
"\"EscapedName\"" -> <|"asd" -> "whee", "number" -> 10|>
],
"JSON"
];
json = JSONStream[$jsSample]
Then here's what Read
looks like on it:
Read@json
{Null, {Opening, Association}}
Table[Read@json, 10]
Table[Read@json, 10]
{{{"\"EscapedName\"", String}, None}, {Null,
Key}, {Null, {Opening, Association}}, {{"asd", String},
None}, {Null, Key}, {{"whee", String}, None}, {{Null, Expression},
Value}, {{"number", String}, None}, {Null, Key}, {{10, Expression},
Value}}
Here's using the data type support:
Table[Read[json, KeyValuePattern], 3] // AbsoluteTiming
{0.015792, {"Mercury" -> <|"Mass" -> "330104000000000000000000. kg",
"Radius" -> "2439.7 km", "Moons" -> <||>|>,
"Venus" -> <|"Mass" -> "4867320000000000000000000. kg",
"Radius" -> "6051.9 km", "Moons" -> <||>|>,
"Earth" -> <|"Mass" -> "5972198600000000000000000. kg",
"Radius" -> "6367.44466 km",
"Moons" -> <|"Moon" -> <|"Mass" -> "73459000000000000000000. kg",
"Radius" -> "1737.5 km"|>|>|>}}
Currently we have Key
, Value
, KeyValuePattern
, Association
, List
, and "Token"
as supported types
I think this would qualify as a proof-of-concept
Here's a package form of it: JSONStream
Association
to cache data about, e.g. whether you've got unmatched braces, and use some combination ofRead
up to various potential tokens, to build a string that can be tested against with a compiled regex and to parse it out like that. Then provide events for when these patterns match. It'd be a bit tedious, but I can see it working with okay performance. $\endgroup$"{"|"}"|"\""|":"
and check to see if you have a complete token or block or whatever, and continue on like that. That's basically the lexing side of things. Then your parser would apply rules to those tokens / blocks. I haven't thought through all of the performance considerations of this strategy though. It might be unworkable. $\endgroup$