diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2025-10-19 00:08:49 +0200 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2025-10-19 00:22:11 +0200 |
| commit | 48bdfbeb03319eb21b5e73e69f525ba298af975c (patch) | |
| tree | 9d189fe570b408e27d82bd434a83b9f0a3bae18e | |
| parent | df56d2eb26b34b0af590f3aedfda7896f4b103dd (diff) | |
json: Add new module
Only has methods for writing JSON for now.
Will let you create invalid json, but should assert if you do.
| -rw-r--r-- | meson.build | 22 | ||||
| -rw-r--r-- | src/json.cc | 305 | ||||
| -rw-r--r-- | src/json.hh | 46 | ||||
| -rw-r--r-- | test/json.cc | 221 |
4 files changed, 594 insertions, 0 deletions
diff --git a/meson.build b/meson.build index 2b74a48..b17ed18 100644 --- a/meson.build +++ b/meson.build @@ -179,6 +179,18 @@ signals_dep = declare_dependency( dependencies: [io_dep, looper_dep], ) +json_lib = library( + 'json', + sources: [ + 'src/json.cc', + 'src/json.hh', + ], + include_directories: inc, +) +json_dep = declare_dependency( + link_with: json_lib, +) + base64_lib = library( 'base64', sources: [ @@ -323,6 +335,16 @@ test('cfg', executable( ], )) +test('json', executable( + 'test_json', + sources: ['test/json.cc'], + include_directories: inc, + dependencies : [ + json_dep, + test_dependencies, + ], +)) + test('base64', executable( 'test_base64', sources: ['test/base64.cc'], diff --git a/src/json.cc b/src/json.cc new file mode 100644 index 0000000..94ff0d4 --- /dev/null +++ b/src/json.cc @@ -0,0 +1,305 @@ +#include "json.hh" + +#include <cassert> +#include <charconv> +#include <cstddef> +#include <cstdint> +#include <memory> +#include <ostream> +#include <string> +#include <string_view> +#include <system_error> +#include <vector> + +namespace json { + +namespace { + +struct StackEntry { + enum class Type : uint8_t { + kRoot, + kObject, + kArray, + }; + + Type type; + bool need_comma{false}; + + explicit StackEntry(Type type) : type(type) {} +}; + +class BaseWriter : public Writer { + public: + BaseWriter() : stack_({StackEntry(StackEntry::Type::kRoot)}) {} + + void value(std::string_view value) override { + before_value(); + quote(value); + } + + void value(int64_t value) override { + before_value(); + write(value); + } + + void value(uint64_t value) override { + before_value(); + write(value); + } + + void value(float value) override { + before_value(); + write(value); + } + + void value(double value) override { + before_value(); + write(value); + } + + void value(bool value) override { + before_value(); + write(value); + } + + void start_array() override { + before_value(); + stack_.emplace_back(StackEntry::Type::kArray); + write('['); + } + + void end_array() override { + if (stack_.empty()) { + assert(false); + return; + } + assert(stack_.back().type == StackEntry::Type::kArray); + stack_.pop_back(); + write(']'); + if (!stack_.empty()) + stack_.back().need_comma = true; + } + + void start_object() override { + before_value(); + stack_.emplace_back(StackEntry::Type::kObject); + write('{'); + } + + void key(std::string_view name) override { + if (stack_.empty()) { + assert(false); + return; + } + assert(stack_.back().type == StackEntry::Type::kObject); + if (stack_.back().need_comma) { + write(','); + stack_.back().need_comma = false; + } + quote(name); + write(':'); + } + + void end_object() override { + if (stack_.empty()) { + assert(false); + return; + } + assert(stack_.back().type == StackEntry::Type::kObject); + stack_.pop_back(); + write('}'); + if (!stack_.empty()) + stack_.back().need_comma = true; + } + + void clear() override { + stack_.clear(); + stack_.emplace_back(StackEntry::Type::kRoot); + } + + protected: + virtual void write(int64_t value) = 0; + virtual void write(uint64_t value) = 0; + virtual void write(float value) = 0; + virtual void write(double value) = 0; + virtual void write(bool value) = 0; + virtual void write(char value) = 0; + virtual void write(std::string_view value) = 0; + + void write(char const* value) { this->write(std::string_view(value)); } + + private: + void before_value() { + if (stack_.empty()) { + assert(false); + return; + } + if (stack_.back().need_comma) { + assert(stack_.back().type != StackEntry::Type::kRoot); + write(','); + } else { + stack_.back().need_comma = true; + } + } + + void quote(std::string_view str) { + write('"'); + size_t last = 0; + while (true) { + auto pos = need_quote(str, last); + if (pos == std::string_view::npos) { + write(str.substr(last)); + break; + } + write(str.substr(last, pos - last)); + switch (str[pos]) { + case '"': + write("\\\""); + break; + case '\\': + write("\\\\"); + break; + case '\n': + write("\\n"); + break; + case '\r': + write("\\r"); + break; + case '\t': + write("\\t"); + break; + case '\b': + write("\\b"); + break; + case '\f': + write("\\f"); + break; + default: { + char tmp[4]; + write("\\u"); + auto [ptr, ec] = std::to_chars(tmp, tmp + sizeof(tmp), str[pos], 16); + if (ec == std::errc()) { + size_t len = ptr - tmp; + assert(len > 0); + for (size_t i = 4; i > len; --i) + write('0'); + write(std::string_view(tmp, len)); + } else { + assert(false); + } + break; + }; + } + last = pos + 1; + } + write('"'); + } + + static inline size_t need_quote(std::string_view str, size_t offset) { + for (; offset < str.size(); ++offset) { + if (str[offset] == '\\' || str[offset] == '"' || + (str[offset] >= 0 && str[offset] < ' ')) + return offset; + } + return std::string_view::npos; + } + + std::vector<StackEntry> stack_; +}; + +class IosWriter : public BaseWriter { + public: + explicit IosWriter(std::ostream& out) : out_(out) {} + + void write(std::string_view value) override { out_ << value; } + + void write(int64_t value) override { out_ << value; } + + void write(uint64_t value) override { out_ << value; } + + void write(float value) override { out_ << value; } + + void write(double value) override { out_ << value; } + + void write(bool value) override { out_ << value; } + + void write(char value) override { out_ << value; } + + private: + std::ostream& out_; +}; + +class StringWriter : public BaseWriter { + public: + explicit StringWriter(std::string& out) : out_(out) {} + + void write(std::string_view value) override { out_.append(value); } + + void write(int64_t value) override { + auto [ptr, ec] = std::to_chars(tmp_, tmp_ + sizeof(tmp_), value); + if (ec == std::errc()) { + out_.append(tmp_, ptr - tmp_); + } else { + assert(false); + } + } + + void write(uint64_t value) override { + auto [ptr, ec] = std::to_chars(tmp_, tmp_ + sizeof(tmp_), value); + if (ec == std::errc()) { + out_.append(tmp_, ptr - tmp_); + } else { + assert(false); + } + } + + void write(float value) override { + auto [ptr, ec] = std::to_chars(tmp_, tmp_ + sizeof(tmp_), value); + if (ec == std::errc()) { + out_.append(tmp_, ptr - tmp_); + } else { + assert(false); + } + } + + void write(double value) override { + auto [ptr, ec] = std::to_chars(tmp_, tmp_ + sizeof(tmp_), value); + if (ec == std::errc()) { + out_.append(tmp_, ptr - tmp_); + } else { + assert(false); + } + } + + void write(bool value) override { out_.append(value ? "true" : "false"); } + + void write(char value) override { out_.push_back(value); } + + void clear() override { + BaseWriter::clear(); + out_.clear(); + } + + private: + std::string& out_; + char tmp_[100]; +}; + +} // namespace + +void Writer::value(char const* value) { this->value(std::string_view(value)); } + +void Writer::value(int value) { + static_assert(sizeof(int) <= sizeof(int64_t)); + this->value(static_cast<int64_t>(value)); +} + +std::unique_ptr<Writer> writer(std::string& out) { + return std::make_unique<StringWriter>(out); +} + +std::unique_ptr<Writer> writer(std::ostream& out) { + return std::make_unique<IosWriter>(out); +} + +} // namespace json diff --git a/src/json.hh b/src/json.hh new file mode 100644 index 0000000..d872dde --- /dev/null +++ b/src/json.hh @@ -0,0 +1,46 @@ +#ifndef JSON_HH +#define JSON_HH + +#include <cstdint> +#include <iosfwd> +#include <memory> +#include <string> +#include <string_view> + +namespace json { + +class Writer { + public: + virtual ~Writer() = default; + + virtual void value(std::string_view value) = 0; + virtual void value(int64_t value) = 0; + virtual void value(uint64_t value) = 0; + virtual void value(float value) = 0; + virtual void value(double value) = 0; + virtual void value(bool value) = 0; + + void value(char const* value); + void value(int value); + + virtual void start_array() = 0; + virtual void end_array() = 0; + + virtual void start_object() = 0; + virtual void key(std::string_view name) = 0; + virtual void end_object() = 0; + + virtual void clear() = 0; + + protected: + Writer() = default; + Writer(Writer const&) = delete; + Writer& operator=(Writer const&) = delete; +}; + +std::unique_ptr<Writer> writer(std::string& out); +std::unique_ptr<Writer> writer(std::ostream& out); + +} // namespace json + +#endif // JSON_HH diff --git a/test/json.cc b/test/json.cc new file mode 100644 index 0000000..2fc7ad2 --- /dev/null +++ b/test/json.cc @@ -0,0 +1,221 @@ +#include "json.hh" + +#include <cstdint> +#include <gtest/gtest.h> +#include <memory> +#include <sstream> +#include <string> +#include <utility> + +namespace { + +enum class Output : uint8_t { + String, + Stream, +}; + +class Writer : public testing::TestWithParam<Output> { + protected: + std::unique_ptr<json::Writer> make() { + switch (GetParam()) { + case Output::String: + return json::writer(str_); + case Output::Stream: + return json::writer(stream_); + } + std::unreachable(); + } + + std::string str() { + switch (GetParam()) { + case Output::String: + return std::move(str_); + case Output::Stream: + return stream_.str(); + } + std::unreachable(); + } + + private: + std::string str_; + std::stringstream stream_; +}; + +} // namespace + +TEST_P(Writer, example1) { + auto writer = make(); + writer->start_object(); + + writer->key("glossary"); + writer->start_object(); + + writer->key("title"); + writer->value("example glossary"); + + writer->key("GlossDiv"); + writer->start_object(); + + writer->key("title"); + writer->value("S"); + + writer->key("GlossList"); + writer->start_object(); + + writer->key("GlossEntry"); + writer->start_object(); + + writer->key("ID"); + writer->value("SGML"); + writer->key("SortAs"); + writer->value("SGML"); + writer->key("GlossTerm"); + writer->value("Standard Generalized Markup Language"); + writer->key("Acronym"); + writer->value("SGML"); + writer->key("Abbrev"); + writer->value("ISO 8879:1986"); + + writer->key("GlossDef"); + writer->start_object(); + + writer->key("para"); + writer->value( + "A meta-markup language, used to create markup languages such as " + "DocBook."); + + writer->key("GlossSeeAlso"); + writer->start_array(); + writer->value("GML"); + writer->value("XML"); + writer->end_array(); + + writer->end_object(); + + writer->key("GlossSee"); + writer->value("markup"); + + writer->end_object(); + + writer->end_object(); + + writer->end_object(); + + writer->end_object(); + + writer->end_object(); + + EXPECT_EQ( + R"({"glossary":{"title":"example glossary","GlossDiv":{"title":"S","GlossList":{"GlossEntry":{"ID":"SGML","SortAs":"SGML","GlossTerm":"Standard Generalized Markup Language","Acronym":"SGML","Abbrev":"ISO 8879:1986","GlossDef":{"para":"A meta-markup language, used to create markup languages such as DocBook.","GlossSeeAlso":["GML","XML"]},"GlossSee":"markup"}}}}})", + str()); +} + +TEST_P(Writer, example2) { + auto writer = make(); + writer->start_object(); + + writer->key("widget"); + writer->start_object(); + + writer->key("debug"); + writer->value("on"); + + writer->key("window"); + writer->start_object(); + + writer->key("title"); + writer->value("Sample Konfabulator Widget"); + writer->key("name"); + writer->value("main_window"); + writer->key("width"); + writer->value(500); + writer->key("height"); + writer->value(500); + + writer->end_object(); + + writer->key("image"); + writer->start_object(); + + writer->key("src"); + writer->value("Images/Sun.png"); + writer->key("name"); + writer->value("sun1"); + writer->key("hOffset"); + writer->value(250); + writer->key("vOffset"); + writer->value(250); + writer->key("alignment"); + writer->value("center"); + + writer->end_object(); + + writer->key("text"); + writer->start_object(); + + writer->key("data"); + writer->value("Click Here"); + writer->key("size"); + writer->value(36); + writer->key("style"); + writer->value("bold"); + writer->key("name"); + writer->value("text1"); + writer->key("hOffset"); + writer->value(250); + writer->key("vOffset"); + writer->value(100); + writer->key("alignment"); + writer->value("center"); + writer->key("onMouseUp"); + writer->value("sun1.opacity = (sun1.opacity / 100) * 90;"); + + writer->end_object(); + + writer->end_object(); + + writer->end_object(); + + EXPECT_EQ( + R"({"widget":{"debug":"on","window":{"title":"Sample Konfabulator Widget","name":"main_window","width":500,"height":500},"image":{"src":"Images/Sun.png","name":"sun1","hOffset":250,"vOffset":250,"alignment":"center"},"text":{"data":"Click Here","size":36,"style":"bold","name":"text1","hOffset":250,"vOffset":100,"alignment":"center","onMouseUp":"sun1.opacity = (sun1.opacity / 100) * 90;"}}})", + str()); +} + +TEST_P(Writer, quote1) { + auto writer = make(); + writer->value(R"("Example text")"); + + EXPECT_EQ(R"("\"Example text\"")", str()); +} + +TEST_P(Writer, quote2) { + auto writer = make(); + writer->value(R"(\"Example text\")"); + + EXPECT_EQ(R"("\\\"Example text\\\"")", str()); +} + +TEST_P(Writer, quote3) { + auto writer = make(); + writer->value(R"( +)"); + + EXPECT_EQ(R"("\n")", str()); +} + +TEST_P(Writer, float) { + auto writer = make(); + writer->value(3.14F); + + EXPECT_EQ("3.14", str()); +} + +TEST_P(Writer, double) { + auto writer = make(); + writer->value(3.14); + + EXPECT_EQ("3.14", str()); +} + +INSTANTIATE_TEST_SUITE_P(AllOutputs, Writer, + testing::Values(Output::String, Output::Stream)); |
