-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement new format API for JSON output #385
Merged
+1,009
−89
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
fb7ac5c
Fix text encoding for large floating point numbers
Neverlord 46d9d64
Implement JSON encoding for broker::data
Neverlord 9044a5d
Implement JSON rendering for data_message
Neverlord 0dd797b
Use new JSON encoding in the WebSocket interface
Neverlord 5a1c185
Fix formatting
Neverlord ca464eb
Fix buffer size we pass to strftime in json.cc
Neverlord d518fa5
Calculate the timestamp value for JSON tests
Neverlord cb6ac57
Update baseline for the web-socket.encrypted btest
Neverlord e75c36f
Fix build on MSVC
Neverlord 427afd1
Integrate review feedback
Neverlord File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,334 @@ | ||
#pragma once | ||
|
||
#include "broker/config.hh" | ||
#include "broker/data.hh" | ||
#include "broker/message.hh" | ||
|
||
#include <algorithm> | ||
#include <array> | ||
#include <cstddef> | ||
#include <cstdint> | ||
#include <string_view> | ||
#include <vector> | ||
|
||
namespace broker::format::json::v1 { | ||
|
||
/// Configures `encode` to render the value as a JSON object, i.e., surround the | ||
/// fields with curly braces. | ||
struct render_object {}; | ||
|
||
/// Configures `encode` to render the value as an embedded JSON object, i.e., | ||
/// omit the curly braces. | ||
struct render_embedded {}; | ||
|
||
/// Appends a string to the output iterator. | ||
template <class OutIter> | ||
OutIter append(std::string_view str, OutIter out) { | ||
return std::copy(str.begin(), str.end(), out); | ||
} | ||
|
||
/// Tag type for selecting the `quoted` overload of `append_encoded`. | ||
struct quoted { | ||
std::string_view str; | ||
}; | ||
|
||
/// Appends a quoted string to the output iterator. Special characters are | ||
/// escaped. | ||
template <class OutIter> | ||
OutIter append(quoted value, OutIter out) { | ||
using namespace std::literals; | ||
*out++ = '"'; | ||
for (auto c : value.str) { | ||
switch (c) { | ||
default: | ||
*out++ = c; | ||
break; | ||
case '\\': | ||
*out++ = '\\'; | ||
*out++ = '\\'; | ||
break; | ||
case '\b': | ||
*out++ = '\\'; | ||
*out++ = 'b'; | ||
break; | ||
case '\f': | ||
*out++ = '\\'; | ||
*out++ = 'f'; | ||
break; | ||
case '\n': | ||
*out++ = '\\'; | ||
*out++ = 'n'; | ||
break; | ||
case '\r': | ||
*out++ = '\\'; | ||
*out++ = 'r'; | ||
break; | ||
case '\t': | ||
*out++ = '\\'; | ||
*out++ = 't'; | ||
break; | ||
case '\v': | ||
*out++ = '\\'; | ||
*out++ = 'v'; | ||
break; | ||
case '"': | ||
*out++ = '\\'; | ||
*out++ = '"'; | ||
break; | ||
} | ||
} | ||
*out++ = '"'; | ||
return out; | ||
} | ||
|
||
/// Appends a field (key-value pair) to the output iterator. | ||
template <class OutIter> | ||
OutIter append_field(std::string_view key, std::string_view val, OutIter out) { | ||
out = append(quoted{key}, out); | ||
*out++ = ':'; | ||
return append(quoted{val}, out); | ||
} | ||
|
||
/// Appends an encoded value to the output iterator. | ||
/// @param value_type The type name of the value. | ||
/// @param value The value. Must provide an overload for `append()`. | ||
/// @param out The output iterator. | ||
template <class Policy = render_object, class Appendable, class OutIter> | ||
OutIter append_encoded(std::string_view value_type, Appendable value, | ||
OutIter out) { | ||
using namespace std::literals; | ||
if constexpr (std::is_same_v<Policy, render_object>) | ||
*out++ = '{'; | ||
out = append(R"_("@data-type":")_", out); | ||
out = append(value_type, out); | ||
out = append(R"_(","data":)_", out); | ||
out = append(value, out); | ||
if constexpr (std::is_same_v<Policy, render_object>) | ||
*out++ = '}'; | ||
return out; | ||
} | ||
|
||
/// Renders the `nil` value to `out` as `{}`. | ||
template <class Policy = render_object, class OutIter> | ||
OutIter encode(none, OutIter out) { | ||
return append_encoded<Policy>("none", "{}", out); | ||
} | ||
|
||
/// Renders a boolean, `count` or `integer` value to `out`. The latter two are | ||
/// rendered using `snprintf`, whereas boolean values are rendered as `true` or | ||
/// `false`. | ||
template <class Policy = render_object, class T, class OutIter> | ||
std::enable_if_t<std::is_integral_v<T>, OutIter> encode(T value, OutIter out) { | ||
using namespace std::literals; | ||
if constexpr (std::is_same_v<T, bool>) { | ||
return append_encoded<Policy>("boolean", value ? "true"sv : "false"sv, out); | ||
} else if constexpr (std::is_same_v<T, count>) { | ||
// An integer can at most have 20 digits (UINT64_MAX). | ||
char buf[24]; | ||
auto size = std::snprintf(buf, 24, "%llu", | ||
static_cast<long long unsigned>(value)); | ||
auto str = std::string_view{buf, static_cast<size_t>(size)}; | ||
return append_encoded<Policy>("count", str, out); | ||
} else { | ||
static_assert(std::is_same_v<T, integer>); | ||
// An integer can at most have 20 digits (UINT64_MAX). | ||
char buf[24]; | ||
auto size = std::snprintf(buf, 24, "%lld", static_cast<long long>(value)); | ||
auto str = std::string_view{buf, static_cast<size_t>(size)}; | ||
return append_encoded<Policy>("integer", str, out); | ||
} | ||
} | ||
|
||
/// Writes `value` to `out` using `snprintf` with the default precision (6 | ||
/// digits), e.g., `0.123456`. | ||
template <class Policy = render_object, class OutIter> | ||
OutIter encode(real value, OutIter out) { | ||
auto size = std::snprintf(nullptr, 0, "%f", value); | ||
if (size < 24) { | ||
char buf[24]; | ||
size = std::snprintf(buf, 24, "%f", value); | ||
auto str = std::string_view{buf, static_cast<size_t>(size)}; | ||
return append_encoded<Policy>("real", str, out); | ||
} else { | ||
std::vector<char> buf; | ||
buf.resize(static_cast<size_t>(size) + 1); // +1 for the null terminator | ||
size = std::snprintf(buf.data(), size + 1, "%f", value); | ||
auto str = std::string_view{buf.data(), static_cast<size_t>(size)}; | ||
return append_encoded<Policy>("real", str, out); | ||
} | ||
} | ||
|
||
/// Renders `value` to `out` as a quoted string. | ||
template <class Policy = render_object, class OutIter> | ||
OutIter encode(std::string_view value, OutIter out) { | ||
return append_encoded<Policy>("string", quoted{value}, out); | ||
} | ||
|
||
/// Renders `value` using the `convert` API and copies the result to `out`. | ||
Neverlord marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// For an IPV4 address, the result is a string in dotted decimal notation, | ||
/// e.g., `192.168.99.9`. For an IPV6 address, the result is a string in | ||
/// hexadecimal notation, e.g., `2001:0db8:85a3::8a2e:0370:7334`. | ||
template <class Policy = render_object, class OutIter> | ||
OutIter encode(const address& value, OutIter out) { | ||
std::string str; | ||
convert(value, str); | ||
return append_encoded<Policy>("address", quoted{str}, out); | ||
} | ||
|
||
/// Renders `value` using the `convert` API and copies the result to `out`. The | ||
/// output string uses the format `prefix/length`, e.g., `192.168.99.0/24`. | ||
template <class Policy = render_object, class OutIter> | ||
OutIter encode(const subnet& value, OutIter out) { | ||
std::string str; | ||
convert(value, str); | ||
return append_encoded<Policy>("subnet", quoted{str}, out); | ||
} | ||
|
||
/// Renders `value` using the `convert` API and copies the result to `out`. | ||
template <class Policy = render_object, class OutIter> | ||
OutIter encode(port value, OutIter out) { | ||
std::string str; | ||
convert(value, str); | ||
return append_encoded<Policy>("port", quoted{str}, out); | ||
} | ||
|
||
// Helper function for encoding timestamps in ISO format, produces a quoted | ||
// string. | ||
size_t encode_to_buf(timestamp value, std::array<char, 32>& buf); | ||
|
||
/// Renders `value` to `out` in millisecond resolution. | ||
template <class Policy = render_object, class OutIter> | ||
OutIter encode(timestamp value, OutIter out) { | ||
std::array<char, 32> buf; | ||
auto size = encode_to_buf(value, buf); | ||
return append_encoded<Policy>("timestamp", std::string_view{buf.data(), size}, | ||
out); | ||
} | ||
|
||
/// Renders `value` to `out` as an integer followed by a suffix, e.g., `42s`. | ||
/// The function picks the highest possible resolution for the given value | ||
/// without losing precision. The suffix is one of `ns`, `us`, `ms`, or `s`. | ||
template <class Policy = render_object, class OutIter> | ||
OutIter encode(timespan value, OutIter out) { | ||
using namespace std::literals; | ||
// Helper function to print the value with a suffix via snprintf. | ||
auto do_encode = [&out](long long val, const char* suffix) { | ||
// An integer can at most have 20 digits (UINT64_MAX) and our suffix may | ||
// only be 2 characters long. | ||
char buf[32]; | ||
auto size = std::snprintf(buf, 32, R"_("%lld%s")_", val, suffix); | ||
auto str = std::string_view{buf, static_cast<size_t>(size)}; | ||
return append_encoded<Policy>("timespan", str, out); | ||
}; | ||
// Short-circuit for zero. Always prints "0s". | ||
auto val = value.count(); | ||
if (val == 0) { | ||
return append_encoded<Policy>("timespan", quoted{"0s"}, out); | ||
} | ||
// Render as nanoseconds if the fractional part is non-zero. | ||
if (val % 1000 != 0) { | ||
return do_encode(val, "ns"); | ||
} | ||
// Render as microseconds if the fractional part is non-zero. | ||
val /= 1000; | ||
if (val % 1000 != 0) { | ||
return do_encode(val, "us"); | ||
} | ||
// Render as milliseconds if the fractional part is non-zero. | ||
val /= 1000; | ||
if (val % 1000 != 0) { | ||
return do_encode(val, "ms"); | ||
} | ||
// Render as seconds otherwise. | ||
val /= 1000; | ||
return do_encode(val, "s"); | ||
} | ||
|
||
/// Copies the name of `value` to `out`. | ||
template <class Policy = render_object, class OutIter> | ||
OutIter encode(const enum_value& value, OutIter out) { | ||
return append_encoded<Policy>("enum-value", quoted{value.name}, out); | ||
} | ||
|
||
/// Encodes a JSON list. | ||
template <class Policy, class ForwardIterator, class Sentinel, class OutIter> | ||
OutIter encode_list(std::string_view type, ForwardIterator first, Sentinel last, | ||
OutIter out); | ||
|
||
/// Renders `value` to `out` as a JSON list. | ||
template <class Policy = render_object, class OutIter> | ||
OutIter encode(const broker::set& values, OutIter out) { | ||
return encode_list<Policy>("set", values.begin(), values.end(), out); | ||
} | ||
|
||
/// Renders `value` to `out` as a JSON list. | ||
template <class Policy = render_object, class OutIter> | ||
OutIter encode(const broker::vector& values, OutIter out) { | ||
return encode_list<Policy>("vector", values.begin(), values.end(), out); | ||
} | ||
|
||
/// Renders `value` to `out` as a sequence, enclosing it in curly braces and | ||
/// displaying key/value pairs as `key -> value`. | ||
template <class Policy = render_object, class OutIter> | ||
OutIter encode(const broker::table& values, OutIter out) { | ||
return encode_list<Policy>("table", values.begin(), values.end(), out); | ||
} | ||
|
||
/// Renders a `data` object to `out` by dispatching to the appropriate overload | ||
/// for the underlying type. | ||
template <class Policy = render_object, class Data, class OutIter> | ||
std::enable_if_t<std::is_same_v<data, Data>, OutIter> encode(const Data& value, | ||
OutIter out) { | ||
return std::visit([&](auto&& x) { return encode<Policy>(x, out); }, | ||
value.get_data()); | ||
} | ||
|
||
/// Renders the key-value pair to `out` as a JSON object. | ||
template <class OutIter> | ||
OutIter encode(const broker::table::value_type& values, OutIter out) { | ||
// Note: this overload needs no policy because it's only used by the | ||
// `encode(const broker::table&, OutIter)` overload and must always | ||
// render sub-objects with enclosing curly braces. | ||
out = append(R"_({"key":)_", out); | ||
out = encode(values.first, out); | ||
out = append(R"_(,"value":)_", out); | ||
out = encode(values.second, out); | ||
*out++ = '}'; | ||
return out; | ||
} | ||
|
||
template <class Policy, class ForwardIterator, class Sentinel, class OutIter> | ||
OutIter encode_list(std::string_view type, ForwardIterator first, Sentinel last, | ||
OutIter out) { | ||
if (first == last) | ||
return append_encoded<Policy>(type, "[]", out); | ||
using namespace std::literals; | ||
if constexpr (std::is_same_v<Policy, render_object>) | ||
*out++ = '{'; | ||
out = append(R"_("@data-type":")_", out); | ||
out = append(type, out); | ||
out = append(R"_(","data":[)_", out); | ||
out = encode(*first, out); | ||
while (++first != last) { | ||
*out++ = ','; | ||
out = encode(*first, out); | ||
} | ||
*out++ = ']'; | ||
if constexpr (std::is_same_v<Policy, render_object>) | ||
*out++ = '}'; | ||
return out; | ||
} | ||
|
||
/// Renders a `data` object to `out`. | ||
template <class OutIter> | ||
OutIter encode(const data_message& msg, OutIter out) { | ||
*out++ = '{'; | ||
out = append(R"_("type":"data-message","topic":)_", out); | ||
out = append(quoted{get_topic(msg).string()}, out); | ||
*out++ = ','; | ||
encode<render_embedded>(get_data(msg), out); | ||
*out++ = '}'; | ||
return out; | ||
} | ||
|
||
} // namespace broker::format::json::v1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Vanilla JavaScript cannot correctly parse numbers so large. Nothing to do, but that would probably be a topic if we had more browsers interact with Zeek.
This even made it into the RFC :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably because JavaScript will store numbers as 64-bit floating point numbers? Probably not much we could do about that. Except sending integers as strings... 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I don't think we should worry too much. If someone actually wants to work with numbers that large, they'll need to use BigInt anyhow and there's
json-bigint
that can parse this properly.Where I saw this first:
https://developer.twitter.com/en/docs/twitter-ids