For a personal project, I need to parse YAML file. I chose to use C-based libYAML instead of yaml-cpp, because:
- 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.
- 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.