Skip to content

Commit

Permalink
feat: add custom format specs for JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
3Hren committed Aug 22, 2016
1 parent 34c18cf commit e7abbdd
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 4 deletions.
12 changes: 12 additions & 0 deletions include/blackhole/formatter/json.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,18 @@ class builder<formatter::json_t> {
auto timestamp(const std::string& pattern) & -> builder&;
auto timestamp(const std::string& pattern) && -> builder&&;

/// Sets the given formatting specification to an attribute with the specified name.
///
/// Custom formatting is useful when it's required to build a JSON tree with preformatted
/// values using a spec, for example, to write a number using HEX representation. Note, that
/// after formatting a value will be written as a string.
///
/// \param name attribute name.
/// \param spec string representation of a spec. The spec should match with libfmt formatting
/// specification.
auto format(std::string name, std::string spec) & -> builder&;
auto format(std::string name, std::string spec) && -> builder&&;

auto build() const && -> std::unique_ptr<formatter_t>;
};

Expand Down
73 changes: 69 additions & 4 deletions src/formatter/json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ struct visitor_t {
rapidjson::StringRef(value.data(), value.size()), allocator);
}

// For non-owning buffers.
// For temporary buffers, rapidjson will copy the data.
auto operator()(const char* data, std::size_t size) -> void {
node.AddMember(rapidjson::StringRef(name.data(), name.size()),
rapidjson::Value(data, static_cast<unsigned int>(size), allocator), allocator);
Expand All @@ -76,6 +76,35 @@ struct visitor_t {
}
};

// TODO: Decompose. Almost the same class is in `string.cpp`.
class view_visitor : public boost::static_visitor<> {
writer_t& writer;
const std::string& spec;

public:
view_visitor(writer_t& writer, const std::string& spec) noexcept :
writer(writer),
spec(spec)
{}

auto operator()(std::nullptr_t) const -> void {
writer.write(spec, "none");
}

template<typename T>
auto operator()(T value) const -> void {
writer.write(spec, value);
}

auto operator()(const string_view& value) const -> void {
writer.write(spec, value.data());
}

auto operator()(const attribute::view_t::function_type& value) const -> void {
value(writer);
}
};

/// A RapidJSON Stream concept implementation required to avoid intermediate buffer allocation.
struct stream_t {
typedef char Ch;
Expand Down Expand Up @@ -109,6 +138,7 @@ class json_t::properties_t {
} routing;

std::unordered_map<std::string, std::string> mapping;
std::unordered_map<std::string, std::string> formatting;

properties_t() :
unique(false),
Expand All @@ -126,6 +156,7 @@ class json_t::inner_t {
std::map<std::string, rapidjson::Pointer> routing;

std::unordered_map<std::string, std::string> mapping;
std::unordered_map<std::string, std::string> formatting;

bool unique;
bool newline;
Expand All @@ -136,6 +167,7 @@ class json_t::inner_t {
inner_t(json_t::properties_t properties) :
rest(properties.routing.unspecified),
mapping(std::move(properties.mapping)),
formatting(std::move(properties.formatting)),
unique(properties.unique),
newline(properties.newline),
timestamp(properties.timestamp),
Expand Down Expand Up @@ -264,19 +296,43 @@ class json_t::inner_t::builder {
auto apply(const string_view& name, const T& value) -> void {
const auto renamed = inner.renamed(name);
visitor_t visitor{inner.get(name, root), root.GetAllocator(), renamed};
visitor(value);

const auto it = inner.formatting.find(std::string(name.data(), name.size()));
if (it != std::end(inner.formatting)) {
writer_t wr;
wr.write(it->second, value);
visitor(wr.inner.data(), wr.inner.size());
} else {
visitor(value);
}
}

auto apply(const string_view& name, const char* data, std::size_t size) -> void {
const auto renamed = inner.renamed(name);
visitor_t visitor{inner.get(name, root), root.GetAllocator(), renamed};
visitor(data, size);

const auto it = inner.formatting.find(std::string(name.data(), name.size()));
if (it != std::end(inner.formatting)) {
writer_t wr;
wr.write(it->second, fmt::StringRef(data, size));
visitor(wr.inner.data(), wr.inner.size());
} else {
visitor(data, size);
}
}

auto apply(const string_view& name, const attribute::view_t& value) -> void {
const auto renamed = inner.renamed(name);
visitor_t visitor{inner.get(name, root), root.GetAllocator(), renamed};
boost::apply_visitor(visitor, value.inner().value);

const auto it = inner.formatting.find(std::string(name.data(), name.size()));
if (it != std::end(inner.formatting)) {
writer_t wr;
boost::apply_visitor(view_visitor(wr, it->second), value.inner().value);
visitor(wr.inner.data(), wr.inner.size());
} else {
boost::apply_visitor(visitor, value.inner().value);
}
}
};

Expand Down Expand Up @@ -426,6 +482,15 @@ auto builder<json_t>::timestamp(const std::string& pattern) && -> builder&& {
return std::move(timestamp(pattern));
}

auto builder<json_t>::format(std::string name, std::string spec) & -> builder& {
d->formatting[name] = spec;
return *this;
}

auto builder<json_t>::format(std::string name, std::string spec) && -> builder&& {
return std::move(format(std::move(name), std::move(spec)));
}

auto builder<json_t>::build() const && -> std::unique_ptr<formatter_t> {
return blackhole::make_unique<json_t>(std::move(*d));
}
Expand Down
57 changes: 57 additions & 0 deletions tests/src/unit/formatter/json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,63 @@ TEST(json_t, NoNewlineByDefault) {
EXPECT_FALSE(json_t().newline());
}

TEST(json_t, MutateSeverityFormat) {
auto formatter = builder<json_t>()
.format("severity", "{:>4}")
.build();

const string_view message("");
const attribute_pack pack;
record_t record(2, message, pack);
writer_t writer;
formatter->format(record, writer);

rapidjson::Document doc;
doc.Parse<0>(writer.result().to_string().c_str());

ASSERT_TRUE(doc.HasMember("severity"));
ASSERT_TRUE(doc["severity"].IsString());
EXPECT_EQ(" 2", std::string(doc["severity"].GetString()));
}

TEST(json_t, MutateThreadFormat) {
auto formatter = builder<json_t>()
.format("thread", "0x123456")
.build();

const string_view message("value");
const attribute_list attributes{{"source", "storage"}};
const attribute_pack pack{attributes};
record_t record(0, message, pack);
writer_t writer;
formatter->format(record, writer);

rapidjson::Document doc;
doc.Parse<0>(writer.result().to_string().c_str());
ASSERT_TRUE(doc.HasMember("thread"));
ASSERT_TRUE(doc["thread"].IsString());
EXPECT_STREQ("0x123456", doc["thread"].GetString());
}

TEST(json_t, MutateAttributeFormat) {
auto formatter = builder<json_t>()
.format("source", "[{:.>6.4}]")
.build();

const string_view message("value");
const attribute_list attributes{{"source", "storage"}};
const attribute_pack pack{attributes};
record_t record(0, message, pack);
writer_t writer;
formatter->format(record, writer);

rapidjson::Document doc;
doc.Parse<0>(writer.result().to_string().c_str());
ASSERT_TRUE(doc.HasMember("source"));
ASSERT_TRUE(doc["source"].IsString());
EXPECT_STREQ("[..stor]", doc["source"].GetString());
}

TEST(builder_t, Newline) {
const auto layout = builder<json_t>()
.newline()
Expand Down

0 comments on commit e7abbdd

Please sign in to comment.