Published on 08/23/2024 09:00 by Stephen Horne
JSON and the Golden Fleece, Pt 4
class JSON {
const std::unique_ptr<ValueNodeBase> head;
public:
JSON() = delete;
JSON(std::unique_ptr<ValueNodeBase>&& up) : head(std::move(up)) {}
Type getType() const {
return head->getType();
}
void* get() const {
return head.get();
}
};
Here
we have our JSON class itself. As you can see, it holds one data member:
head. As its name suggests, this holds a pointer to the head of the data tree.
I know. Groundbreaking. Its getType() method just calls its topmost
ValueNode’s getType() (via the nifty dereference-and-call shorthand ->)
and returns the value.
The way I wrote its constructor is very deliberate. It takes an r-value
reference to a unique_ptr which owns a ValueNodeBase object and creates a
new unique_ptr which then takes ownership of the ValueNodeBase and assigns
this new unique_ptr to its private head member. The magic is in
unique_ptr’s move constructor: it handles passing ownership over to the new
instance and setting its own internal reference to the object it previously
owned to nullptr. It is then safe to throw away.
The reason for this is that I do not want a JSON object that is empty, because
that’s an invalid state. I also don’t want to make a new JSON data tree when a
JSON class instance is created, because that’s wasteful. After parsing, I’ll
build up the data tree and holding a pointer to it, ready to hand it off. Then,
when the parsing has finished and has succeeded, a new JSON object will be
created that owns the already-existing data tree. This, I think, will be pretty
efficient time- and spacewise.
Parsing
Foolish of me to think I was ready to being the parsing task without this noble
JSON class to own the final tree, keeping it safe in a harsh digital world. I
think that now we’re ready to get into the fun part. We’ll need two stacks of
pointers: one for keeping our parsed data together, and one for collecting our
ObjectNode and ArrayNode internal values when a } or ] event occurs. The
stacks will be typed std::stack<std::unique_ptr<StackElement>>. StackElement
will look something like this
struct StackElement {
std::pair<unsigned, unsigned> loc; // tracking where (line,column) we are in the payload
std::unique_ptr<std::string> key; // will be `nullptr` to signal "just a value"
std::unique_ptr<ValueNodeBase> value; // always pointing to some value
};
The JSON::Parser::parse() method will take an istream of any kind
(better remember to call ::is_open() on any ifstreams before providing
them :-|), create a char variable, and begin reading the stream one blessed
line at a time, then one byte (“wide” unicode character support can be added
later) at a time. A rough idea of some events, and their responses, that I plan
to implement are:
ncharacter encountered: pull the next three bytes out of the stream and check againstull. If they matche, make aNullNodeand put aStackElementpointing to it on the main stack. Else, throw an exception.fcharacter encountered: pull the next four out, compare againstfalse, and create a newBooleanNode(false)instance if all is well. Put aStackElementpointing to it on the main stack. Else, throw."character encountered: Well, this is either a key in an object or a string value. Pull bytes out of the stream until another, non-escaped,"is encountered. Make a string with what we have so far. Take a byte off until either a:,,},]orEOFis encountered. Ignore whitespace. If we encounter anything else, throw.- If
:is encountered, this is hopefully a key. We add aStackElementto the stack whose key points to this new string, with anullptrvalue. - If
,and the current topStackElementin the main stack either has anullptrkey and a non-nullptrvalue, or both key and values set, we keep taking characters off the stack. Else, we throw.- If}or], we moveStackElements over to the second stack until we encounter anObjectNodeorArrayNode` on the main stack, respectively. - If
EOFand we’re either in “still building a string” or “waiting for a value to go with a key” state, we throw.
- If
And so on and so forth. There will be a pointer head holding on to the first
value created. The stack should be empty when EOF is reached, else we throw.
This (I think) works because as ObjectNodes and ArrayNodes are wrapped up,
we take them off the stack and either set head to them, if head == nullptr,
or check the getType() method on what’s there.
- If
ObjectNodeand theStackElementat the top of the stack has anullptrkey, we throw. Very naughty. - If
ArrayNodeand theStackElementat the top of the stack has a non-nullptrkey, we throw. Also verboten. - If anything else, we’ve already parsed a value and put it on the tree, but it isn’t able to contain anything. The JSON payload is ruled invalid, and we throw.
Written by Stephen Horne
← Back to blog