4
\$\begingroup\$

For a personal project, I need to parse YAML file. I chose to use C-based libYAML instead of yaml-cpp, because:

  1. I try to keep my dependencies to what's commonly installed and Debian statistics show 45% installs already have libYAML, vs 0.5% for yaml-cpp.
  2. I don't need variant node types, as the config parser that uses this wrapper builds the final datastructures directly using a state machine.

I wrote this lightweight wrapper around the library.

The wrapper aims at encapsulating all the details of the C library, throwing exceptions when errors arise and leaking no resources in the process. In addition, its header must not have any libYAML-related stuff in it, so I can change the underlying implementation later should I want to.

Provided API is an inheritable class that converts parsing events into virtual method calls, a la hierarchical visitor pattern, but without conditional navigation.

Here comes the API (YAMLParser.h):

#ifndef YAML_PARSER_H
#define YAML_PARSER_H

#include <stddef.h>
#include <exception>
#include <istream>
#include <string>

class YAMLParser
{
public:
    class ParseError : public std::exception
    {
    public:
                    ParseError(const std::string & what, size_t line, size_t col);
                    ParseError(const std::string & what, size_t line, size_t col,
                               const std::string & context, size_t ctx_line);
        const char *what() const noexcept override { return m_what.c_str(); }
    protected:
        std::string m_what;
    };

public:
    virtual         ~YAMLParser() {}
    virtual void    parse(std::istream & stream);

protected:
    virtual void    streamStart() = 0;
    virtual void    streamEnd() = 0;
    virtual void    documentStart() = 0;
    virtual void    documentEnd() = 0;
    virtual void    sequenceStart(const std::string & tag, const std::string & anchor) = 0;
    virtual void    sequenceEnd() = 0;
    virtual void    mappingStart(const std::string & tag, const std::string & anchor) = 0;
    virtual void    mappingEnd() = 0;
    virtual void    alias(const std::string & anchor) = 0;
    virtual void    scalar(const std::string & value, const std::string & tag,
                           const std::string & anchor) = 0;

    ParseError      makeError(const std::string & what) const
                    { return ParseError(what, m_line, m_column); }
private:
    size_t          m_line;
    size_t          m_column;
};

#endif

And here comes the implementation:

#include <yaml.h>
#include <istream>
#include <memory>
#include <sstream>
#include "myproject/YAMLParser.h"

/****************************************************************************/
// Helpers

/** Wrapper class for yaml_parser_t object
 *
 * Using a wrapper ensures all resources will be properly freed even
 * if an exception is throw during parsing.
 */
class Parser final
{
public:
    Parser(std::istream & stream)
    {
        m_done = false;
        m_hasEvent = false;
        if (yaml_parser_initialize(&m_parser) == 0) {
            throw std::runtime_error("YAML parser initialization failed");
        }
        yaml_parser_set_input(&m_parser, readHandler, &stream);
    }

    ~Parser()
    {
        if (m_hasEvent) { yaml_event_delete(&event); }
        yaml_parser_delete(&m_parser);
    }

    void parseNext()
    {
        if (m_hasEvent) {
            yaml_event_delete(&event);
            m_hasEvent = false;
        }
        if (yaml_parser_parse(&m_parser, &event) == 0) {
            throw YAMLParser::ParseError(
                m_parser.problem,
                m_parser.problem_mark.line, m_parser.problem_mark.column,
                m_parser.context, m_parser.context_mark.line
            );
        }
        m_hasEvent = true;
        if (event.type == YAML_STREAM_END_EVENT) { m_done = true; }
    }

    bool done() const { return m_done; }

private:
    static int readHandler(void * stream_ptr, unsigned char * buffer,
                           size_t size, size_t * size_read)
    {
        std::istream & stream = *reinterpret_cast<std::istream*>(stream_ptr);
        if (!stream.eof()) {
            stream.read(reinterpret_cast<std::istream::char_type*>(buffer), size);
            *size_read = stream.gcount();
        } else {
            *size_read = 0;
        }
        return stream.bad() ? 0 : 1;
    }

public:
    yaml_event_t    event;

private:
    bool            m_done;         ///< Set when end of stream was reached
    yaml_parser_t   m_parser;       ///< libYAML parser object
    bool            m_hasEvent;     ///< Set when @ref event holds an event
};

/****************************************************************************/

static const char * toStr(const yaml_char_t * str) {
    if (str == NULL) { return ""; }
    return reinterpret_cast<const char *>(str);
}

void YAMLParser::parse(std::istream & stream)
{
    auto parser = Parser(stream);

    while (!parser.done()) {
        parser.parseNext();
        m_line = parser.event.start_mark.line;
        m_column = parser.event.start_mark.column;

        switch (parser.event.type) {
        case YAML_STREAM_START_EVENT:   streamStart(); break;
        case YAML_STREAM_END_EVENT:     streamEnd(); break;
        case YAML_DOCUMENT_START_EVENT: documentStart(); break;
        case YAML_DOCUMENT_END_EVENT:   documentEnd(); break;
        case YAML_SEQUENCE_START_EVENT: {
            const auto & data = parser.event.data.sequence_start;
            sequenceStart(toStr(data.tag), toStr(data.anchor));
            break;
        }
        case YAML_SEQUENCE_END_EVENT:   sequenceEnd(); break;
        case YAML_MAPPING_START_EVENT: {
            const auto & data = parser.event.data.mapping_start;
            mappingStart(toStr(data.tag), toStr(data.anchor));
            break;
        }
        case YAML_MAPPING_END_EVENT:    mappingEnd(); break;
        case YAML_ALIAS_EVENT: {
            const auto & data = parser.event.data.alias;
            alias(toStr(data.anchor));
            break;
        }
        case YAML_SCALAR_EVENT: {
            const auto & data = parser.event.data.scalar;
            scalar(std::string(toStr(data.value), data.length),
                   toStr(data.tag), toStr(data.anchor));
            break;
        }
        default:
            throw std::logic_error("unexpected event type");
        }
    }
}

/****************************************************************************/

YAMLParser::ParseError::ParseError(const std::string & what, size_t line, size_t col)
{
    std::ostringstream error;
    error <<what <<" line " <<line + 1 <<" column " <<col + 1;
    m_what = error.str();
}

YAMLParser::ParseError::ParseError(const std::string & what, size_t line, size_t col,
                                   const std::string & context, size_t ctx_line)
{
    std::ostringstream error;
    error <<what
          <<" line " <<line + 1 <<" column " <<col + 1
          <<" " <<context <<" from line " <<ctx_line + 1;
    m_what = error.str();
}

As I am updating my rusty knowledge of C++, trying to take advantage of C++11 as well, I would appreciate comments: interface / implementation mistakes, style errors, ways to make it more canonical, …

I would also be interested in finding potential leaks that may be caused by exceptions or non-obvious corner cases. All yaml_*_t objects, once initialized, must be destroyed exactly once using the appropriate yaml_*_delete functions.

\$\endgroup\$
  • \$\begingroup\$ You may to look at my YAML parser (that wraps libyaml). github.com/Loki-Astari/ThorsSerializer/blob/master/doc/… The example shows JSON but it handles YAML in the same way. It provides a C++11 interface for reading/writing YAML objects using a simple declaration). \$\endgroup\$ – Martin York Apr 14 '17 at 17:05
  • \$\begingroup\$ That sounds pretty cool, I will definitely have a look at it. \$\endgroup\$ – spectras Apr 14 '17 at 18:00
2
\$\begingroup\$

Exceptions

There is no need to write your own what() method.

The class std::exception and std::runtime_error both accept a string in the constructor that defines the error message returned by what(). So there is no need to define your own version of this method:

    class ParseError : public std::runtime_error
    {
        public:
            ParseError(const std::string & what, size_t line, size_t col,
                       const std::string & context = "", size_t ctx_line = -1)
                : std::runtime_error(genErrMsg(what, line, col, context, ctx_line))
            {}

        private:
            std::string static genErrMsg(const std::string & what, size_t line, size_t col,
                                         const std::string & context, size_t ctx_line)
            {
                std::ostringstream error;
                error << what
                      << " line " << line + 1 << " column " << col + 1
                      << " " << context;
                if (ctx_line != -1) {
                    error << " from line " << ctx_line + 1;
                }
                return error.str();
            }
    };
\$\endgroup\$
  • \$\begingroup\$ Great! I was not quite sure how to put it together as several ways seemed to work. Your looks much cleaner, I'll use it. \$\endgroup\$ – spectras Apr 14 '17 at 17:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.