Stephen Horne

← Back to blog

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:

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.

Written by Stephen Horne

← Back to blog