Eclog-CPP is a cross-platform C++ library for parsing and rendering Eclog (a JSON-like format) texts.
- C++98 compatible
- No external dependencies
- No complex build system
- Supports both exceptions and error codes
- Supports in-place parsing
- Compatibility
- Integration
- Memory Management
- Thread Safety
- Error Handling
- Quick Start Guide
- Reference
The following platform and compiler combinations have been tested:
- Linux using g++ 5.4/7.5
- Linux using Clang 6.0/11.1
- Windows using Visual C++ 8/10/14/15/16
- macOS using Xcode 12
- iOS (macOS using Xcode 12)
- Android (Windows using Android Studio 4.1.2)
Eclog-CPP is self-contained; no specific build process is required. To integrate Eclog-CPP into your project, first add the Include
directory to the header search path. Then add the source files located in
the Source
directory to the sources. That's all.
A portable and compact version is also provided; all header files and source files are merged into two files, Eclog.h
and Eclog.cpp
, in the Compact
directory. You can easily copy the two files into your project, which allows all the classes and functions to be used by including only one header file in your code.
Dynamic container classes in Eclog-CPP allow you to have memory management through the use of allocators. Each dynamic container has a template parameter specifying the allocator to be used. You can specify different memory management schemes on either a global or per-container basis by overriding the default allocator template parameter (See Allocator Requirements and Memory Management Configuration).
Eclog-CPP offers conditional thread safety. It is safe to make concurrent use of distinct objects but unsafe to make concurrent use of a single object.
Eclog-CPP supports both general error handling approaches: exceptions and error codes. You'll see that many functions and class methods in Eclog-CPP are overloaded on the two different error reporting mechanisms. For example,
template<typename Handler>
void parse(Context& ctx, Handler& handler);
template<typename Handler>
void parse(Context& ctx, Handler& handler, ErrorCode& ec);
Generally speaking, overloaded functions with an ErrorCode parameter use error codes to report errors, while overloaded functions without an ErrorCode parameter use exceptions to report errors.
An example of using exceptions to handle errors:
try {
eclog::parse(ctx, handler);
}
catch (eclog::ParseErrorException& e)
{
std::cout << "Parse error at line " << e.lineNumber()
<< ", column " << e.columnNumber()
<< ", " << e.what() << std::endl;
}
catch (eclog::Exception& e)
{
std::cout << e.what() << std::endl;
}
An example of using error codes to handle errors:
eclog::ErrorCode ec;
eclog::parse(ctx, handler, ec);
if (ec == eclog::ec_parse_error)
{
const eclog::ParseError& e = eclog::cast<eclog::ParseError>(ec.error());
std::cout << "Parse error at line " << e.line()
<< ", column " << e.column()
<< ", " << e.message() << std::endl;
}
else if (ec)
{
std::cout << ec.message() << std::endl;
}
Error codes and exceptions in Eclog-CPP are nearly equivalent except for critical errors. Most critical errors (or faults), such as logic errors and out-of-memory errors, are unrecoverable at runtime. Hence a function with an ErrorCode parameter may also throw exceptions.
If exceptions are unavailable in your project, you can suppress the throwing of exceptions (see Error Handling Configuration).
Event-based parsing is simple: when an element is encountered, the parsing engine reports it directly to the handler you specify. It is more efficient than tree-based parsing for many types of applications. Eclog-CPP offers two parsing APIs for different situations.
The parse function is the primary interface of event-based parsing, enabling you to go through the elements of an Eclog text in a single function call.
You provide an input stream (InputStream instance) that specifies the input source and a parsing buffer (ParsingBuffer instance) used to hold intermediate results during the parsing. Next, use the input stream and parsing buffer instances to create a parsing context (Context instance). Finally, call parse with the parsing context and your handler as the arguments. For example,
[Note: Eclog-CPP provides several implementations of InputStream and ParsingBuffer; you can also use custom implementations when necessary.]
std::fstream fs("Person.ecl", std::ios::in | std::ios::binary);
eclog::StdStreamInputStream stream(fs);
eclog::DynamicParsingBuffer buffer;
eclog::Context ctx(stream, buffer);
MyHandler handler;
eclog::parse(ctx, handler);
The handler requirements of parse are syntax-oriented; any class that satisfies the requirements can be used as a handler class. For instance,
class MyHandler {
public:
void onObjectBegin();
void onObjectEnd();
void onArrayBegin();
void onArrayEnd();
void onKey(const eclog::Key& key);
void onValue(const eclog::Value& value);
};
Given the following example Eclog file,
# Person.ecl
firstName: John
lastName: Smith
age: 27
address:
{
streetAddress: "21 2nd Street"
city: "New York"
state: NY
postalCode: "10021-3100"
}
phoneNumbers:
[
{ type: home, number: "212 555-1234" }
{ type: office, number: "646 555-4567" }
]
your handler will receive the following sequence of events during the parsing process:
onObjectBegin
onKey "firstName"
onValue "John"
onKey "lastName"
onValue "Smith"
onKey "age"
onValue 27
onKey "address"
onObjectBegin
onKey "streetAddress"
onValue "21 2nd Street"
onKey "city"
onValue "New York"
onKey "state"
onValue "NY"
onKey "postalCode"
onValue "10021-3100"
onObjectEnd
onKey "phoneNumbers"
onArrayBegin
onObjectBegin
onKey "type"
onValue "home"
onKey "number"
onValue "212 555-1234"
onObjectEnd
onObjectBegin
onKey "type"
onValue "office"
onKey "number"
onValue "646 555-4567"
onObjectEnd
onArrayEnd
onObjectEnd
As an alternative interface to parse, the parseObject and parseArray functions allow you to handle only the top-level members of an object or array in the corresponding handler. This can be handy for producing more readable code, especially when working with lambda expressions.
First, create a parsing context in the same way as detailed in the previous section. Then, call parseObject with the parsing context and your handler as the arguments. This example takes a lambda expression as the handler:
const char* text = "a: Foo, b: {c: true, d: [1, 2]}, e: [3, Bar], f: null";
eclog::MemoryInputStream stream(text, strlen(text));
eclog::DynamicParsingBuffer buffer;
eclog::Context ctx(stream, buffer);
eclog::parseObject(ctx, [&](eclog::Key key, eclog::Value value)
{
std::cout << key << std::endl;
});
The output of the above code is:
a
b
e
f
As you can see, the handler receives only the top-level members. When you need to go deep into a nested object or array, call parseObject or parseArray at that point. Let's modify the example above to retrieve array d
's members:
const char* text = "a: Foo, b: {c: true, d: [1, 2]}, e: [3, Bar], f: null";
eclog::MemoryInputStream stream(text, strlen(text));
eclog::DynamicParsingBuffer buffer;
eclog::Context ctx(stream, buffer);
eclog::parseObject(ctx, [&](eclog::Key key, eclog::Value value)
{
if (key == "b")
{
eclog::parseObject(ctx, [&](eclog::Key key, eclog::Value value)
{
if (key == "d")
{
eclog::parseArray(ctx, [&](eclog::Value value)
{
std::cout << value.asNumber() << std::endl;
});
}
});
}
});
The output from the modified code is
1
2
If you want to terminate a parsing process, call Context::terminate in your handler. For example,
const char* text = "a: Apple, b: Banana, c: Lemon, d: Pear, e: Orange";
eclog::MemoryInputStream stream(text, strlen(text));
eclog::DynamicParsingBuffer buffer;
eclog::Context ctx(stream, buffer);
try
{
eclog::parseObject(ctx, [&](eclog::Key key, eclog::Value value)
{
std::cout << value.asString() << std::endl;
if (key == "c") {
ctx.terminate();
}
});
}
catch (eclog::ParseErrorException& e)
{
if (e.error() == eclog::pe_user_asked_for_termination) {
std::cout << "(terminated)" << std::endl;
}
}
The output of the above code is the following:
Apple
Banana
Lemon
(terminated)
Eclog-CPP supports in-place parsing, which is parsing an Eclog text stored in a contiguous memory buffer without dynamic memory allocations. Consider the following example:
char array[2048];
size_t length = loadFile("Person.ecl", array, sizeof(array) - 1);
eclog::MemoryInputStream stream(array, length);
eclog::InplaceParsingBuffer buffer(array, sizeof(array));
eclog::Context ctx(stream, buffer);
MyHandler handler;
eclog::parse(ctx, handler);
As the above code shows, InplaceParsingBuffer and MemoryInputStream can safely share the same memory buffer—although InplaceParsingBuffer stores intermediate results in the shared buffer, it will never overwrite the part that MemoryInputStream is going to read.
[Note: In extreme cases, the buffer length required by InplaceParsingBuffer is 1 byte more than MemoryInputStream. The reason for this is that there is a hidden null character after the last character of all strings returned by parsing buffers to make the strings compatible with C APIs.]
Rendering is the reverse of parsing, meaning it is the process of converting data into an Eclog text. Eclog-CPP offers renderer classes to help you render Eclog texts efficiently.
The renderer classes (i.e., Renderer, ObjectRenderer, and ArrayRenderer) allow you to render (or compose) Eclog texts in the immediate mode.
You provide an output stream (OutputStream instance) that specifies the output target. Then, create the primary renderer (Renderer instance) from the output stream. Next, use object renderers (ObjectRenderer instances) and array renderers (ArrayRenderer instances) to render objects, arrays, and their members in the order you go through the Eclog text. Finally, close the primary renderer.
This is an example of rendering an Eclog text to the standard output:
[Note: Eclog-CPP provides several implementations of OutputStream; you can also use custom implementations when necessary.]
eclog::StdStreamOutputStream stream(std::cout);
eclog::Renderer renderer(stream);
renderer.beginRootObject();
{
eclog::ObjectRenderer root(renderer);
root.renderMember("title", "Main Window");
root.renderMember("id", eclog::StringDesc("main_window",
eclog::string_notation_unquoted));
root.renderMember("width", 400);
root.renderMember("height", 400);
}
renderer.endRootObject();
renderer.close();
The output of the above code is
title: "Main Window"
id: main_window
width: 400
height: 400
You may notice that this example renders the root object with an object renderer created from the primary renderer. Renderers are handled in a hierarchical structure. To render an object, you first begin the object's rendering. Then, create a new object renderer from the renderer that began the rendering. Next, render its members by alternating calls to renderMember
on the renderer. Finally, end the object's rendering. The steps for rendering an array are similar to those for rendering an object.
Let's modify the example above to add a nested object:
eclog::StdStreamOutputStream stream(std::cout);
eclog::Renderer renderer(stream);
renderer.beginRootObject();
{
eclog::ObjectRenderer root(renderer);
root.renderMember("title", "Main Window");
root.renderMember("id", eclog::StringDesc("main_window",
eclog::string_notation_unquoted));
root.beginObject("menu");
{
eclog::ObjectRenderer menu(root);
menu.renderMember("id", eclog::StringDesc("main_menu",
eclog::string_notation_unquoted));
menu.beginArray("items");
{
eclog::ArrayRenderer items(menu);
items.renderMember("File");
items.renderMember("Edit");
}
menu.endArray();
}
root.endObject();
root.renderMember("width", 400);
root.renderMember("height", 400);
}
renderer.endRootObject();
renderer.close();
The output of the above code is
title: "Main Window"
id: main_window
menu:
{
id: main_menu
items:
[
"File"
"Edit"
]
}
width: 400
height: 400
You can configure an Eclog text's rendering by providing the primary renderer with a renderer configuration (RendererConfig instance). Let's modify the example in the previous section to make it create a JSON-compatible Eclog text:
eclog::StdStreamOutputStream stream(std::cout);
eclog::RendererConfig rc(eclog::RendererConfig::scheme_json_compatible);
rc.indentCharacter = eclog::RendererConfig::indent_character_space;
rc.indentSize = 2;
rc.placeOpenBracketOnNewLineForObjects = false;
rc.placeOpenBracketOnNewLineForArrays = false;
eclog::Renderer renderer(stream, rc);
renderer.beginRootObject();
{
eclog::ObjectRenderer root(renderer);
root.renderMember("title", "Main Window");
root.renderMember("id", eclog::StringDesc("main_window",
eclog::string_notation_unquoted));
root.beginObject("menu");
{
eclog::ObjectRenderer menu(root);
menu.renderMember("id", eclog::StringDesc("main_menu",
eclog::string_notation_unquoted));
menu.beginArray("items");
{
eclog::ArrayRenderer items(menu);
items.renderMember("File");
items.renderMember("Edit");
}
menu.endArray();
}
root.endObject();
root.renderMember("width", 400);
root.renderMember("height", 400);
}
renderer.endRootObject();
renderer.close();
The output of the above code is
{
"title": "Main Window",
"id": "main_window",
"menu": {
"id": "main_menu",
"items": [
"File",
"Edit"
]
},
"width": 400,
"height": 400
}
A document (Document instance) is a dynamic container that stores an Eclog text in memory as a tree; each key and value is stored as a typed node in the document tree. The Document class implements the ObjectNode interface; hence, we can say a document is an object node (ObjectNode instance), the root node. By using documents, you can easily load, access, modify, and save Eclog texts.
To load an Eclog text into a document (or an object node), use the parse
method. For example,
eclog::Document doc;
// Load from a string
doc.parse("server: localhost, ports: [8000, 8001], connection_max: 5000");
Another example is the following:
eclog::Document doc;
std::fstream is("Config.ecl", std::ios::in | std::ios::binary);
// Load from a file
doc.parse(is);
To save a document (or an object node) as an Eclog text, use the render
method. For example,
eclog::Document doc;
// ...
std::fstream os("Config.ecl", std::ios::out | std::ios::binary);
// Save to a file
doc.render(os);
If you want to access an element of a document (or an object node), you can use the find
or get
methods.
An example of accessing elements of a document with type-constrained find
methods:
// Find a string value by key
eclog::ObjectNode::Iterator it = doc.findString("firstName");
if (it != doc.end())
{
eclog::StringNode& firstName = it->value().asString();
std::cout << firstName.value() << std::endl;
}
// Find a number value by key
it = doc.findNumber("age");
if (it != doc.end())
{
eclog::NumberNode& age = it->value().asNumber();
std::cout << age.value() << std::endl;
}
An example of accessing elements of a document with type-constrained get
methods:
try
{
// Get a string value by key
std::cout << doc.getString("firstName").value() << std::endl;
// Get a number value by key
std::cout << doc.getNumber("age").value() << std::endl;
}
catch (eclog::OutOfRangeException& e)
{
// ...
}
catch (eclog::BadCastException& e)
{
// ...
}
You can replace the get
methods with the getOrAdd
methods to ensure that an element exists before accessing it. For example,
// If the object node doesn't exist, an empty object is added
eclog::ObjectNode& address = doc.getOrAddObject("address");
// You can specify the default value through the second argument
eclog::StringNode& city = address.getOrAddString("city", "New York");
You can access an element of an array node (ArrayNode instance) by referring to its index, such as in this example:
eclog::ArrayNode& pNums = doc.getArray("phoneNumbers");
for (size_t i = 0; i < pNums.size(); ++i)
{
// Get the nth element of the array
eclog::ArrayNode::Element& element = pNums.at(i);
// ...
}
This example accesses an array element by using a type-constrained get
method:
if (!pNums.empty())
{
// Get the first object node of the array
eclog::ObjectNode& pNum = pNums.getObject(0);
// ...
}
You can iterate over an object node or array node by using iterators or C++11 range-based for loop, as in the following:
for (eclog::ObjectNode::Iterator it = doc.begin(); it != doc.end(); ++it)
{
eclog::ObjectNode::Element& element = *it;
std::cout << element.key().str() << std::endl;
}
eclog::ArrayNode& pNums = doc.getArray("phoneNumbers");
for (eclog::ArrayNode::Iterator it = pNums.begin(); it != pNums.end(); ++it)
{
eclog::ArrayNode::Element& element = *it;
// ...
}
This is an example of C++11 range-based for loop:
for (auto& element : doc)
{
std::cout << element.key().str() << std::endl;
}
for (auto& element : doc.getArray("phoneNumbers"))
{
// ...
}
If you want to add a new element into an object node or array node, you can use the append
or insert
methods. For example,
eclog::Document doc;
doc.append("title", "Main Window");
// Add an empty object
doc.append("menu", eclog::empty_object);
eclog::ObjectNode& menu = doc.last().value().asObject();
// Add an empty array
menu.append("items", eclog::empty_array);
eclog::ArrayNode& menuItems = menu.last().value().asArray();
menuItems.append("File");
menuItems.append("Edit");
// Insert a string before a given position
doc.insert(doc.find("menu"), "id",
eclog::StringDesc("main_window", eclog::string_notation_unquoted));
doc.append("width", 400);
doc.append("height", 400);
If you want to remove an element from an object node or array node, you can use the overloads of the remove
method. For example,
// Remove a value from an object by key
doc.remove("title");
// Remove a value from an object by position
doc.remove(doc.begin());
// Remove a value from an array by index
menuItems.remove(1);
You can update the value of an element by using the updateValue
method, as in the following example:
doc.get("width").updateValue(500);
doc.get("height").updateValue(500);
// This will replace the array with a null value
menu.get("items").updateValue(eclog::null);
Eclog-CPP supports merge patches and conforms to the processing rules defined in the JSON Merge Patch (RFC 7396) standard. For example, given the following original Eclog text,
a: b, c: {d: e, f: g}
changing the value of a
and removing f
can be described with this merge patch:
a: z, c: {f: null}
To apply a merge patch to a target document, call the merge
method on the target document with the merge patch document as the argument, like this:
eclog::Document doc;
doc.parse("a: b, c: {d: e, f: g}");
eclog::Document patch;
patch.parse("a: z, c: {f: null}");
doc.merge(patch);
std::cout << doc.toStdString();
The output of the above code is
a: z
c:
{
d: e
}
I/O Streams | |||
---|---|---|---|
Abstract Classes InputStream OutputStream |
Classes MemoryInputStream StdStreamInputStream MemoryOutputStream StdStreamOutputStream StdStringOutputStream |
Parsing Buffers | |||
---|---|---|---|
Abstract Classes ParsingBuffer |
Classes DynamicParsingBuffer InplaceParsingBuffer |
Misc | |||
---|---|---|---|
Utility UTF8Decoder UTF8Encoder |
Requirements Allocator Requirements |
Macros Memory Management Configuration Error Handling Configuration |