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:
n
character encountered: pull the next three bytes out of the stream and check againstull
. If they matche, make aNullNode
and put aStackElement
pointing to it on the main stack. Else, throw an exception.f
character encountered: pull the next four out, compare againstfalse
, and create a newBooleanNode(false)
instance if all is well. Put aStackElement
pointing 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:
,,
}
,]
orEOF
is encountered. Ignore whitespace. If we encounter anything else, throw.- If
:
is encountered, this is hopefully a key. We add aStackElement
to the stack whose key points to this new string, with anullptr
value. - If
,
and the current topStackElement
in the main stack either has anullptr
key and a non-nullptr
value, or both key and values set, we keep taking characters off the stack. Else, we throw.- If
}or
], we move
StackElements over to the second stack until we encounter an
ObjectNodeor
ArrayNode` on the main stack, respectively. - If
EOF
and 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 ObjectNode
s and ArrayNode
s 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
ObjectNode
and theStackElement
at the top of the stack has anullptr
key
, we throw. Very naughty. - If
ArrayNode
and theStackElement
at the top of the stack has a non-nullptr
key
, 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