From 53255d1e23d1c62f54c663fcb0e6378b05032d5b Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 24 Dec 2024 21:22:34 +0100 Subject: [PATCH] Hello libcode-json --- .editorconfig | 17 + .gitattributes | 1 + .gitea/workflows/on-push.yaml | 24 + .gitignore | 31 + LICENSE | 31 + README.md | 21 + build/.gitignore | 4 + build/bootstrap.build | 7 + build/export.build | 6 + build/root.build | 16 + buildfile | 5 + code/json/.gitignore | 9 + code/json/.swp | Bin 0 -> 12288 bytes code/json/buildfile | 65 ++ code/json/diagnostics.cxx | 45 ++ code/json/diagnostics.hxx | 43 + code/json/emitter.hxx | 54 ++ code/json/emitter.ixx | 141 ++++ code/json/emitter.test.cxx | 115 +++ code/json/exception.cxx | 43 + code/json/exception.hxx | 59 ++ code/json/marshaling/mapping.hxx | 245 ++++++ code/json/marshaling/marshaling-context.cxx | 8 + code/json/marshaling/marshaling-context.hxx | 13 + code/json/marshaling/marshaling-traits.hxx | 626 +++++++++++++++ .../marshaling/marshaling-traits.test.cxx | 146 ++++ code/json/marshaling/marshaling.hxx | 29 + code/json/marshaling/serialize.hxx | 91 +++ code/json/marshaling/serialize.test.cxx | 60 ++ code/json/marshaling/traits.hxx | 72 ++ code/json/optional.hxx | 30 + code/json/parser.hxx | 758 ++++++++++++++++++ code/json/pointer.cxx | 163 ++++ code/json/pointer.hxx | 41 + code/json/pointer.test.cxx | 95 +++ code/json/read.hxx | 74 ++ code/json/read.test.cxx | 607 ++++++++++++++ code/json/resolve.test.cxx | 23 + code/json/variant.hxx | 285 +++++++ code/json/variant.ixx | 409 ++++++++++ code/json/variant.txx | 19 + code/json/version.hxx.in | 37 + code/json/write.hxx | 31 + code/json/write.test.cxx | 31 + manifest | 13 + repositories.manifest | 10 + tests/.gitignore | 8 + tests/build/.gitignore | 4 + tests/build/bootstrap.build | 5 + tests/build/root.build | 16 + tests/buildfile | 1 + 51 files changed, 4687 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitea/workflows/on-push.yaml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build/.gitignore create mode 100644 build/bootstrap.build create mode 100644 build/export.build create mode 100644 build/root.build create mode 100644 buildfile create mode 100644 code/json/.gitignore create mode 100644 code/json/.swp create mode 100644 code/json/buildfile create mode 100644 code/json/diagnostics.cxx create mode 100644 code/json/diagnostics.hxx create mode 100644 code/json/emitter.hxx create mode 100644 code/json/emitter.ixx create mode 100644 code/json/emitter.test.cxx create mode 100644 code/json/exception.cxx create mode 100644 code/json/exception.hxx create mode 100644 code/json/marshaling/mapping.hxx create mode 100644 code/json/marshaling/marshaling-context.cxx create mode 100644 code/json/marshaling/marshaling-context.hxx create mode 100644 code/json/marshaling/marshaling-traits.hxx create mode 100644 code/json/marshaling/marshaling-traits.test.cxx create mode 100644 code/json/marshaling/marshaling.hxx create mode 100644 code/json/marshaling/serialize.hxx create mode 100644 code/json/marshaling/serialize.test.cxx create mode 100644 code/json/marshaling/traits.hxx create mode 100644 code/json/optional.hxx create mode 100644 code/json/parser.hxx create mode 100644 code/json/pointer.cxx create mode 100644 code/json/pointer.hxx create mode 100644 code/json/pointer.test.cxx create mode 100644 code/json/read.hxx create mode 100644 code/json/read.test.cxx create mode 100644 code/json/resolve.test.cxx create mode 100644 code/json/variant.hxx create mode 100644 code/json/variant.ixx create mode 100644 code/json/variant.txx create mode 100644 code/json/version.hxx.in create mode 100644 code/json/write.hxx create mode 100644 code/json/write.test.cxx create mode 100644 manifest create mode 100644 repositories.manifest create mode 100644 tests/.gitignore create mode 100644 tests/build/.gitignore create mode 100644 tests/build/bootstrap.build create mode 100644 tests/build/root.build create mode 100644 tests/buildfile diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ec9f3de --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +indent_size = 4 +max_line_length = off +trim_trailing_whitespace = false + +[*.yaml] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitea/workflows/on-push.yaml b/.gitea/workflows/on-push.yaml new file mode 100644 index 0000000..23a9e11 --- /dev/null +++ b/.gitea/workflows/on-push.yaml @@ -0,0 +1,24 @@ +name: on-push +on: [push] + +jobs: + build-and-test: + runs-on: linux + container: code.helloryan.se/infra/buildenv/cxx-amd64-fedora-40:latest + volumes: + - /build + steps: + - name: Clone repository + uses: actions/checkout@v3 + - name: Authenticate + run: | + git config unset http.https://code.helloryan.se/.extraheader + echo "${{ secrets.NETRC }}" >> ~/.netrc + - name: Initialize + run: | + bpkg create -d /build cc config.cc.coptions="-Wall -Werror" + bdep init -A /build + - name: Build + run: b + - name: Test + run: b test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c96e1ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +.bdep/ + +# Local default options files. +# +.build2/local/ + +# Compiler/linker output. +# +*.d +*.t +*.i +*.i.* +*.ii +*.ii.* +*.o +*.obj +*.gcm +*.pcm +*.ifc +*.so +*.dylib +*.dll +*.a +*.lib +*.exp +*.pdb +*.ilk +*.exe +*.exe.dlls/ +*.exe.manifest +*.pc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dfc745b --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright © 2024 Ryan. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. All advertising materials mentioning features or use of this software must + display the following acknowledgement: + + This product includes software developed by Ryan, http://helloryan.se/. + +4. Neither the name(s) of the copyright holder(s) nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a3b1ea9 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# libcode-json + +![Build status](https://code.helloryan.se/code/libcode-json/actions/workflows/on-push.yaml/badge.svg) + +## Requirements + +None, other than a modern C++-compiler. + +## Building + +See the wiki, https://code.helloryan.se/code/wiki/wiki/Build-Instructions, for +build instructions. + +## Contact + +Please report bugs and issues by sending an e-mail to: ryan@helloryan.se. + +## Contributing + +Please send an e-mail to ryan@helloryan.se to request an account and +write-access to the libcode-json repository. diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..974e01d --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,4 @@ +/config.build +/root/ +/bootstrap/ +build/ diff --git a/build/bootstrap.build b/build/bootstrap.build new file mode 100644 index 0000000..c4c495d --- /dev/null +++ b/build/bootstrap.build @@ -0,0 +1,7 @@ +project = libcode-json + +using version +using config +using test +using install +using dist diff --git a/build/export.build b/build/export.build new file mode 100644 index 0000000..a996327 --- /dev/null +++ b/build/export.build @@ -0,0 +1,6 @@ +$out_root/ +{ + include code/json/ +} + +export $out_root/code/json/$import.target diff --git a/build/root.build b/build/root.build new file mode 100644 index 0000000..21e0a2e --- /dev/null +++ b/build/root.build @@ -0,0 +1,16 @@ +# Uncomment to suppress warnings coming from external libraries. +# +#cxx.internal.scope = current + +cxx.std = latest + +using cxx + +hxx{*}: extension = hxx +ixx{*}: extension = ixx +txx{*}: extension = txx +cxx{*}: extension = cxx + +# The test target for cross-testing (running tests under Wine, etc). +# +test.target = $cxx.target diff --git a/buildfile b/buildfile new file mode 100644 index 0000000..bbe185e --- /dev/null +++ b/buildfile @@ -0,0 +1,5 @@ +./: {code/ tests/} doc{README.md} legal{LICENSE} manifest + +# Don't install tests. +# +tests/: install = false diff --git a/code/json/.gitignore b/code/json/.gitignore new file mode 100644 index 0000000..b1ed0e0 --- /dev/null +++ b/code/json/.gitignore @@ -0,0 +1,9 @@ +# Generated version header. +# +version.hxx + +# Unit test executables and Testscript output directories +# (can be symlinks). +# +*.test +test-*.test diff --git a/code/json/.swp b/code/json/.swp new file mode 100644 index 0000000000000000000000000000000000000000..c66ef3ec80d2abe28af9e086de59b6b05115d137 GIT binary patch literal 12288 zcmeI&zYf7r6vy$yA|}FQD`tO+#UmJu?3YRuX`3pE$=i4+3j-t1;FMYnMnb+f`Q4M7 z-sIf-*_w9L9QOKFw{DA$d^|lysgag&W4!13U5%!$HxGhqOZ0YOY`>W8^$#ciPGZ9&vh3lATVg-xUGLGB;gsCDr literal 0 HcmV?d00001 diff --git a/code/json/buildfile b/code/json/buildfile new file mode 100644 index 0000000..5efc067 --- /dev/null +++ b/code/json/buildfile @@ -0,0 +1,65 @@ +intf_libs = # Interface dependencies. +impl_libs = # Implementation dependencies. +test_libs = # Test dependencies. + +import intf_libs =+ libcode-unicode%lib{code-unicode} + +./: lib{code-json}: libul{code-json} + +libul{code-json}: {hxx ixx txx cxx}{** -**.test... -version} \ + {hxx }{ version} + +libul{code-json}: $impl_libs $intf_libs + +# Unit tests. +# +exe{*.test}: +{ + test = true + install = false +} + +for t: cxx{**.test...} +{ + d = $directory($t) + n = $name($t)... + + ./: $d/exe{$n}: $t $d/{hxx ixx txx}{+$n} $d/testscript{+$n} $test_libs + $d/exe{$n}: libul{code-json}: bin.whole = false +} + +hxx{version}: in{version} $src_root/manifest +{ + dist = true + clean = ($src_root != $out_root) +} + +# Build options. +# +cxx.poptions =+ "-I$out_root" "-I$src_root" + +# Export options. +# +lib{code-json}: +{ + cxx.export.poptions = "-I$out_root" "-I$src_root" + cxx.export.libs = $intf_libs +} + +# For pre-releases use the complete version to make sure they cannot +# be used in place of another pre-release or the final version. See +# the version module for details on the version.* variable values. +# +if $version.pre_release + lib{code-json}: bin.lib.version = "-$version.project_id" +else + lib{code-json}: bin.lib.version = "-$version.major.$version.minor" + +# Install into the code/json/ subdirectory of, say, /usr/include/ +# recreating subdirectories. +# +{hxx ixx txx}{*}: +{ + install = include/code/json/ + install.subdirs = true +} diff --git a/code/json/diagnostics.cxx b/code/json/diagnostics.cxx new file mode 100644 index 0000000..e0b571e --- /dev/null +++ b/code/json/diagnostics.cxx @@ -0,0 +1,45 @@ +#include + +namespace code::json { + +std::vector< std::pair< diagnostics::location, std::string > > const& +diagnostics::warnings() const +{ + return warnings_; +} + +std::vector< std::pair< diagnostics::location, std::string > > const& +diagnostics::errors() const +{ + return errors_; +} + +void +diagnostics::warning(location loc, std::string description) +{ + warnings_.emplace_back(std::make_pair(loc, description)); +} + +void +diagnostics::error(location loc, std::string description) +{ + errors_.emplace_back(std::make_pair(loc, description)); +} + +std::ostream& +operator<<(std::ostream& o, diagnostics const& d) +{ + for (auto const& j : d.warnings()) { + o << "warning: " << j.first.line << ':' << j.first.column << ": " + << j.second << '\n'; + } + + for (auto const& j : d.errors()) { + o << "error: " << j.first.line << ':' << j.first.column << ": " << j.second + << '\n'; + } + + return o; +} + +} // namespace code::json diff --git a/code/json/diagnostics.hxx b/code/json/diagnostics.hxx new file mode 100644 index 0000000..63670c4 --- /dev/null +++ b/code/json/diagnostics.hxx @@ -0,0 +1,43 @@ +#ifndef code__json__diagnostics_hxx_ +#define code__json__diagnostics_hxx_ + +#include +#include +#include +#include +#include + +namespace code::json { + +class diagnostics +{ +public: + struct location + { + std::uint32_t line; + std::uint32_t column; + }; + + std::vector< std::pair< diagnostics::location, std::string > > const& + warnings() const; + + std::vector< std::pair< diagnostics::location, std::string > > const& + errors() const; + + void + warning(location loc, std::string description); + + void + error(location loc, std::string description); + +private: + std::vector< std::pair< location, std::string > > warnings_; + std::vector< std::pair< location, std::string > > errors_; +}; + +std::ostream& +operator<<(std::ostream& o, diagnostics const& d); + +} // namespace code::json + +#endif diff --git a/code/json/emitter.hxx b/code/json/emitter.hxx new file mode 100644 index 0000000..8611bcc --- /dev/null +++ b/code/json/emitter.hxx @@ -0,0 +1,54 @@ +#ifndef code__json__emitter_hxx_ +#define code__json__emitter_hxx_ + +#include + +#include +#include + +namespace code::json { + +class emitter { +public: + explicit emitter(std::ostream& output); + + std::ostream& + output(); + + void + operator()(variant::undefined_t const& value); + + void + operator()(bool const& value); + + void + operator()(long long int const& value); + + void + operator()(unsigned long long int const& value); + + void + operator()(long double const& value); + + void + operator()(std::string const& value); + + void + operator()(std::vector< variant > const& value); + + void + operator()(std::map< std::string, variant > const& value); + +private: + void + do_indent(); + + std::ostream& output_; + std::size_t indent_{ 0 }; +}; + +} // namespace code::json + +#include + +#endif diff --git a/code/json/emitter.ixx b/code/json/emitter.ixx new file mode 100644 index 0000000..5747b10 --- /dev/null +++ b/code/json/emitter.ixx @@ -0,0 +1,141 @@ +namespace code::json { + +inline emitter::emitter(std::ostream& output) : output_{ output } +{} + +inline std::ostream& +emitter::output() +{ + return output_; +} + +inline void +emitter::operator()(variant::undefined_t const& value) +{ + output() << "null"; +} + +inline void +emitter::operator()(bool const& value) +{ + output() << (value ? "true" : "false"); +} + +inline void +emitter::operator()(long long int const& value) +{ + output() << std::to_string(value); +} + +inline void +emitter::operator()(unsigned long long int const& value) +{ + output() << std::to_string(value); +} + +inline void +emitter::operator()(long double const& value) +{ + output() << std::to_string(value); +} + +inline void +emitter::operator()(std::string const& value) +{ + output() << '\"'; + + for (auto const j : value) { + switch (j) { + case '\n': + output() << "\\n"; + break; + case '\t': + output() << "\\t"; + break; + case '\\': + output() << "\\"; + break; + case '"': + output() << "\\\""; + break; + // case '\x22': + // case '\x5c': + // case '\x2f': + // case '\x62': + // case '\x66': + // case '\x6e': + // case '\x72': + // case '\x74': + default: + output() << j; + } + } + + output() << '\"'; +} + +inline void +emitter::operator()(std::vector< variant > const& value) +{ + if (value.empty()) { + output() << "[]"; + return; + } + + output() << '[' << '\n'; + + ++indent_; + + bool first{ true }; + for (auto const& j : value) { + if (!first) + output() << ',' << '\n'; + do_indent(); + first = false; + visit(*this, j.value_); + } + output() << '\n'; + --indent_; + + do_indent(); + output() << ']'; +} + +inline void +emitter::operator()(std::map< std::string, variant > const& value) +{ + if (value.empty()) { + output() << "{}"; + return; + } + + output() << "{\n"; + + ++indent_; + + bool first{ true }; + for (auto const& j : value) { + if (!first) + output() << ",\n"; + first = false; + do_indent(); + (*this)(j.first); + output() << ':' << ' '; + visit(*this, j.second.value_); + } + output() << '\n'; + --indent_; + + do_indent(); + output() << '}'; +} + +inline void +emitter::do_indent() +{ + std::size_t i{ indent_ * 2 }; + while (i-- > 0) + output() << ' '; +} + +} // namespace code::json diff --git a/code/json/emitter.test.cxx b/code/json/emitter.test.cxx new file mode 100644 index 0000000..476f231 --- /dev/null +++ b/code/json/emitter.test.cxx @@ -0,0 +1,115 @@ +#include + +#include +#include + +#define DEFINE_TEST(x) std::cout << x << '\n'; +#define TEST_EQUAL(x, y) if ((x) != (y)) return __LINE__; + +int +main() +{ + DEFINE_TEST("undefined") + { + std::stringstream str; + code::json::emitter emitter{ str }; + emitter(code::json::variant::undefined_t{}); + + TEST_EQUAL(str.str(), "null"); + } + + DEFINE_TEST("boolean: true") + { + std::stringstream str; + code::json::emitter emitter{ str }; + emitter(true); + + TEST_EQUAL(str.str(), "true"); + } + + DEFINE_TEST("boolean: false") + { + std::stringstream str; + code::json::emitter emitter{ str }; + emitter(false); + + TEST_EQUAL(str.str(), "false"); + } + + DEFINE_TEST("signed number") + { + std::stringstream str; + code::json::emitter emitter{ str }; + emitter(-10LL); + + TEST_EQUAL(str.str(), "-10"); + } + + DEFINE_TEST("unsigned number") + { + std::stringstream str; + code::json::emitter emitter{ str }; + emitter(100LL); + + TEST_EQUAL(str.str(), "100"); + } + + DEFINE_TEST("real number") + { + std::stringstream str; + code::json::emitter emitter{ str }; + emitter(1.2L); + + TEST_EQUAL(str.str(), "1.2"); + } + + DEFINE_TEST("string") + { + std::stringstream str; + code::json::emitter emitter{ str }; + emitter(std::string{ "hello" }); + + TEST_EQUAL(str.str(), "\"hello\""); + } + + DEFINE_TEST("array: []") + { + std::stringstream str; + code::json::emitter emitter{ str }; + emitter(std::vector< code::json::variant >{}); + + TEST_EQUAL(str.str(), "[]"); + } + + DEFINE_TEST("array: [null]") + { + std::stringstream str; + code::json::emitter emitter{ str }; + + emitter(std::vector< code::json::variant >{ code::json::variant{} }); + + TEST_EQUAL(str.str(), "[\n null\n]"); + } + + DEFINE_TEST("object: {}") + { + std::stringstream str; + code::json::emitter emitter{ str }; + + emitter(std::map< std::string, code::json::variant >{}); + + TEST_EQUAL(str.str(), "{}"); + } + + DEFINE_TEST("object: {...}") + { + std::stringstream str; + code::json::emitter emitter{ str }; + + emitter(std::map< std::string, code::json::variant >{ { "", {} } }); + + TEST_EQUAL(str.str(), "{\n \"\": null\n}"); + } + + return 0; +} diff --git a/code/json/exception.cxx b/code/json/exception.cxx new file mode 100644 index 0000000..1eebd42 --- /dev/null +++ b/code/json/exception.cxx @@ -0,0 +1,43 @@ +#include + +namespace code::json { + +invalid_json::invalid_json(diagnostics::location const& loc, + std::string const& what) + : std::runtime_error{ make_error_string(loc, what) }, location_{ loc } +{} + +diagnostics::location const& +invalid_json::location() const +{ + return location_; +} + +std::string +invalid_json::make_error_string(diagnostics::location const& loc, + std::string const& what) +{ + std::ostringstream str; + str << loc.column << " " << loc.line << ": " << what; + return str.str(); +} + +invalid_type::invalid_type() : std::runtime_error{ "invalid type" } +{} + +invalid_syntax::invalid_syntax() : std::runtime_error{ "invalid syntax" } +{} + +unexpected_type::unexpected_type() + : std::runtime_error{ "unexpected JSON type" } +{} + +invalid_object_key::invalid_object_key(std::string const& key) + : std::runtime_error{ "invalid key '" + key + "'" } +{} + +invalid_array_index::invalid_array_index(std::string const& index) + : std::runtime_error{ "invalid key '" + index + "'" } +{} + +} // namespace code::json diff --git a/code/json/exception.hxx b/code/json/exception.hxx new file mode 100644 index 0000000..918a6b0 --- /dev/null +++ b/code/json/exception.hxx @@ -0,0 +1,59 @@ +#ifndef code__json__exception_hxx_ +#define code__json__exception_hxx_ + +#include + +#include +#include +#include + +namespace code::json { + +/// Exception-class used to indicated invalid JSON. +class invalid_json : public std::runtime_error { +public: + invalid_json(diagnostics::location const& loc, std::string const& what); + + diagnostics::location const& + location() const; + + static std::string + make_error_string(diagnostics::location const& loc, std::string const& what); + +private: + diagnostics::location location_; +}; + +/// Exception-class used to indicate an invalid type. +class invalid_type : public std::runtime_error { +public: + invalid_type(); +}; + +/// Exception class used to indicate invalid JSON pointer syntax. +class invalid_syntax : public std::runtime_error { +public: + invalid_syntax(); +}; + +/// Exception class used to indicate an unexpected JSON type. +class unexpected_type : public std::runtime_error { +public: + unexpected_type(); +}; + +/// Exception-class used to indicate an object key. +class invalid_object_key : public std::runtime_error { +public: + explicit invalid_object_key(std::string const& key); +}; + +/// Exception-class used to indicate an invalid array index. +class invalid_array_index : public std::runtime_error { +public: + explicit invalid_array_index(std::string const& index); +}; + +} // namespace code::json + +#endif diff --git a/code/json/marshaling/mapping.hxx b/code/json/marshaling/mapping.hxx new file mode 100644 index 0000000..ffdaa2d --- /dev/null +++ b/code/json/marshaling/mapping.hxx @@ -0,0 +1,245 @@ +#ifndef json__marshaling__mapping_hxx_ +#define json__marshaling__mapping_hxx_ + +#include +#include +#include + +#include +#include + +#include +#include + +namespace code::json::marshaling { + +template< typename T > +class member_mapping { +public: + using getter_type = std::function< variant(T const&, marshaling_context*) >; + using setter_type = std::function< void(T&, variant const&, marshaling_context*) >; + + member_mapping(std::string key, + bool optional, + getter_type getter, + setter_type setter) + : key_{ std::move(key) }, + optional_{ optional }, + getter_{ std::move(getter) }, + setter_{ std::move(setter) } + {} + + member_mapping(pointer p, + bool optional, + getter_type getter, + setter_type setter) + : key_{ std::move(p) }, + optional_{ optional }, + getter_{ std::move(getter) }, + setter_{ std::move(setter) } + {} + + template< typename U > + member_mapping(member_mapping< U > const& other) + : key_{other.key_}, + optional_{other.optional_}, + getter_{other.getter_}, + setter_{other.setter_} + {} + + std::variant const& + key() const + { + return key_; + } + + bool + optional() const + { + return optional_; + } + + variant + get(T const& instance, marshaling_context* context) const + { + return getter_(instance, context); + } + + void + set(T& instance, variant const& value, marshaling_context* context) const + { + setter_(instance, value, context); + } + +private: + template< typename U > + friend class member_mapping; + + std::variant key_; + bool optional_; + getter_type getter_; + setter_type setter_; +}; + +template< typename T, typename M > +member_mapping< T > +member(std::string key, M T::*member) +{ + return member_mapping< T >{ + std::move(key), + is_optional_v, + [member](T const& instance, marshaling_context* context) -> json::variant + { + return marshaling_traits< M >::marshal(instance.*member, context); + }, + [member](T& instance, json::variant const& v, marshaling_context* context) + { + instance.*member = marshaling_traits< M >::unmarshal(v, context); + } + }; +} + +template< typename T, typename M > +member_mapping< T > +member(pointer p, M T::*member) +{ + return member_mapping< T >{ + std::move(p), + is_optional_v, + [member](T const& instance, marshaling_context* context) -> json::variant + { + return marshaling_traits< M >::marshal(instance.*member, context); + }, + [member](T& instance, json::variant const& v, marshaling_context* context) + { + instance.*member = marshaling_traits< M >::unmarshal(v, context); + } + }; +} + +template< typename T, typename Accessor > +member_mapping< T > +accessor(std::string key, Accessor access) +{ + return member_mapping< T >{ + std::move(key), + is_optional_v()))>>, + [access](T const& instance, marshaling_context* context) -> json::variant + { + using M = std::decay_t; + return marshaling_traits< M >::marshal(access(instance), context); + }, + [access](T& instance, json::variant const& v, marshaling_context* context) + { + using M = std::decay_t; + access(instance) = marshaling_traits< M >::unmarshal(v, context); + } + }; +} + +template< typename T, typename Accessor > +member_mapping< T > +accessor(pointer p, Accessor access) +{ + return member_mapping< T >{ + std::move(p), + is_optional_v()))>>, + [access](T const& instance, marshaling_context* context) -> json::variant + { + using M = std::decay_t; + return marshaling_traits< M >::marshal(access(instance), context); + }, + [access](T& instance, json::variant const& v, marshaling_context* context) + { + using M = std::decay_t; + access(instance) = marshaling_traits< M >::unmarshal(v, context); + } + }; +} + +template< typename T, typename V > +member_mapping< T > +static_value(std::string key, V value) +{ + return member_mapping< T >{ + std::move(key), + true, + [value](T const& instance, marshaling_context* context) -> json::variant { + return marshaling_traits< V >::marshal(value, context); + }, + [](T&, json::variant const&, marshaling_context*) { + // No-op. + } + }; +} + +template< typename T, typename V > +member_mapping< T > +static_value(pointer p, V value) +{ + return member_mapping< T >{ + std::move(p), + true, + [value](T const& instance, marshaling_context* context) -> json::variant { + return marshaling_traits< V >::marshal(value, context); + }, + [](T&, json::variant const&, marshaling_context*) { + // No-op. + } + }; +} + +template< typename T > +class mapping { +public: + using container_type = std::vector< member_mapping< T > >; + using const_iterator = typename container_type::const_iterator; + + mapping(container_type mappings) + : mappings_{std::move(mappings)} + {} + + mapping(std::initializer_list< member_mapping< T > > init) + { + for (auto const& j : init) + mappings_.emplace_back(j); + } + + const_iterator + begin() const + { + return mappings_.begin(); + } + + const_iterator + end() const + { + return mappings_.end(); + } + +private: + container_type mappings_; + +}; + +template +mapping +map(std::initializer_list> init) +{ + typename mapping::container_type m{init}; + + auto inherit = [&](auto&& base) + { + for (auto const& j : base) { + m.emplace_back(j); + } + }; + + ((inherit(Bases::json())), ...); + + return m; +} + +} // namespace code::json::marshaling + +#endif diff --git a/code/json/marshaling/marshaling-context.cxx b/code/json/marshaling/marshaling-context.cxx new file mode 100644 index 0000000..5eb4aed --- /dev/null +++ b/code/json/marshaling/marshaling-context.cxx @@ -0,0 +1,8 @@ +#include + +namespace code::json::marshaling { + +marshaling_context::~marshaling_context() +{} + +} // namespace code::json::marshaling diff --git a/code/json/marshaling/marshaling-context.hxx b/code/json/marshaling/marshaling-context.hxx new file mode 100644 index 0000000..5718ab7 --- /dev/null +++ b/code/json/marshaling/marshaling-context.hxx @@ -0,0 +1,13 @@ +#ifndef code__json__marshaling__marshaling_context_hxx_ +#define code__json__marshaling__marshaling_context_hxx_ + +namespace code::json::marshaling { + +class marshaling_context { +protected: + virtual ~marshaling_context(); +}; + +} // namespace code::json::marshaling + +#endif diff --git a/code/json/marshaling/marshaling-traits.hxx b/code/json/marshaling/marshaling-traits.hxx new file mode 100644 index 0000000..61c584e --- /dev/null +++ b/code/json/marshaling/marshaling-traits.hxx @@ -0,0 +1,626 @@ +#ifndef code__json__marshaling__marshaling_traits_hxx_ +#define code__json__marshaling__marshaling_traits_hxx_ + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include // TODO: Remove. +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace code::json::marshaling { + +template< typename T > +struct marshaling_traits { + using model_type = T; + + static variant + marshal(model_type const& model, marshaling_context* context) + { + variant v{std::map{}}; + + // TODO: Handle mappings with pointers. + for (auto const& mapping : model_type::json()) { + if (std::holds_alternative(mapping.key())) { + auto ptr = std::get(mapping.key()); + ptr.write(v, mapping.get(model, context)); + } + else { + v.set(std::get(mapping.key()), mapping.get(model, context)); + } + } + + return v; + } + + static model_type + unmarshal(variant const& value, marshaling_context* context) + { + if constexpr (std::is_default_constructible_v< model_type >) { + // TODO: Change exception type. + if (!value.is_object()) + throw std::runtime_error{ "cannot unmarshal non-object value" }; + + model_type model; + for (auto const& mapping : model_type::json()) { + if (std::holds_alternative(mapping.key())) { + auto ptr = std::get(mapping.key()); + auto v = ptr.read(value); + + if (!v && !mapping.optional()) + // FIXME: Add ptr to exception. + throw std::runtime_error{ "missing field '" + to_string(ptr) + "'" }; + else + mapping.set(model, *v, context); + continue; + } + + auto const& key = std::get(mapping.key()); + + if (!value.contains(key)) { + if (!mapping.optional()) + // TODO: Change exception type. + throw std::runtime_error{ "missing field '" + key + "'" }; + + continue; // Try next key. + } + + mapping.set(model, value.get(key), context); + } + + return model; + } + else { + throw std::runtime_error{ "this shouldn't compile" }; + } + } +}; + +template< typename T > +struct marshaling_traits< optional< T > > { + using model_type = optional< T >; + + static variant + marshal(optional< T > const& model, marshaling_context* context) + { + if (model) + return marshaling_traits< T >::marshal(*model, context); + + return {}; + } + + static optional< T > + unmarshal(variant const& value, marshaling_context* context) + { + if (value.is_undefined()) + return {}; + + return marshaling_traits< T >::unmarshal(value, context); + } +}; + +template< typename... T > +struct marshaling_traits< std::variant< T... > > { + using model_type = std::variant< T... >; + + static variant + marshal(std::variant< T... > const& model, marshaling_context* context) + { + variant result; + + auto visitor = [&result, context](const auto& obj) + { + using type = std::decay_t; + result = marshaling_traits::marshal(obj, context); + result.set("$type", std::decay_t::type_identifier); + }; + + std::visit(visitor, model); + + return result; + } + + template + struct unmarshaler + { + static + std::variant + unmarshal(variant const& value, marshaling_context* context) + { + if (!value.contains("$type")) + throw std::invalid_argument{"variant missing '$type' identifier"}; + + if (value.get("$type").get_string() == Current::type_identifier) + return marshaling_traits< Current >::unmarshal(value, context); + + if constexpr (sizeof...(Next) > 0) + return unmarshaler::unmarshal(value, context); + + throw std::invalid_argument{"couldn't unmarshal value"}; + } + }; + + static std::variant< T... > + unmarshal(variant const& value, marshaling_context* context) + { + return unmarshaler::unmarshal(value, context); + } +}; + +template<> +struct marshaling_traits< bool > { + using model_type = bool; + + static variant + marshal(bool model, marshaling_context*) + { + return model; + } + + static bool + unmarshal(variant const& value, marshaling_context*) + { + if (!value.is_boolean()) + throw std::runtime_error{ "not a boolean" }; + + return value.get_boolean(); + } +}; + +template<> +struct marshaling_traits< short int > { + using model_type = short int; + + static variant + marshal(short int model, marshaling_context*) + { + return model; + } + + static short int + unmarshal(variant const& value, marshaling_context*) + { + if (!value.is_number()) + throw std::runtime_error{ "not a number" }; + + return value.get_number< short int >(); + } +}; + +template<> +struct marshaling_traits< int > { + using model_type = int; + + static variant + marshal(int model, marshaling_context*) + { + return model; + } + + static int + unmarshal(variant const& value, marshaling_context*) + { + if (!value.is_number()) + throw std::runtime_error{ "not a number" }; + + return value.get_number< int >(); + } +}; + +template<> +struct marshaling_traits< long int > { + using model_type = long int; + + static variant + marshal(long int model, marshaling_context*) + { + return model; + } + + static long int + unmarshal(variant const& value, marshaling_context*) + { + if (!value.is_number()) + throw std::runtime_error{ "not a number" }; + + return value.get_number< long int >(); + } +}; + +template<> +struct marshaling_traits< long long int > { + using model_type = long long int; + + static variant + marshal(long long int model, marshaling_context*) + { + return model; + } + + static long long int + unmarshal(variant const& value, marshaling_context*) + { + if (!value.is_number()) + throw std::runtime_error{ "not a number" }; + + return value.get_number< long long int >(); + } +}; + +template<> +struct marshaling_traits< unsigned short int > { + using model_type = unsigned short int; + + static variant + marshal(unsigned short int model, marshaling_context*) + { + return model; + } + + static unsigned short int + unmarshal(variant const& value, marshaling_context*) + { + if (!value.is_number()) + throw std::runtime_error{ "not a number" }; + + return value.get_number< unsigned short int >(); + } +}; + +template<> +struct marshaling_traits< unsigned int > { + using model_type = unsigned int; + + static variant + marshal(unsigned int model, marshaling_context*) + { + return model; + } + + static unsigned int + unmarshal(variant const& value, marshaling_context*) + { + if (!value.is_number()) + throw std::runtime_error{ "not a number" }; + + return value.get_number< unsigned int >(); + } +}; + +template<> +struct marshaling_traits< unsigned long int > { + using model_type = unsigned long int; + + static variant + marshal(unsigned long int model, marshaling_context*) + { + return model; + } + + static unsigned long int + unmarshal(variant const& value, marshaling_context*) + { + if (!value.is_number()) + throw std::runtime_error{ "not a number" }; + + return value.get_number< unsigned long int >(); + } +}; + +// unsigned long long int + +template<> +struct marshaling_traits< unsigned long long int > { + using model_type = unsigned long long int; + + static variant + marshal(unsigned long long int model, marshaling_context*) + { + return model; + } + + static unsigned long long int + unmarshal(variant const& value, marshaling_context*) + { + if (!value.is_number()) + throw std::runtime_error{ "not a number" }; + + return value.get_number< unsigned long long int >(); + } +}; + +template<> +struct marshaling_traits< float > { + using model_type = float; + + static variant + marshal(float model, marshaling_context*) + { + return model; + } + + static float + unmarshal(variant const& value, marshaling_context*) + { + if (!value.is_number()) + throw std::runtime_error{ "not a number" }; + + return value.get_number< float >(); + } +}; + +template<> +struct marshaling_traits< double > { + using model_type = double; + + static variant + marshal(double model, marshaling_context*) + { + return model; + } + + static double + unmarshal(variant const& value, marshaling_context*) + { + if (!value.is_number()) + throw std::runtime_error{ "not a number" }; + + return value.get_number< double >(); + } +}; + +template<> +struct marshaling_traits< long double > { + using model_type = long double; + + static variant + marshal(long double model, marshaling_context*) + { + return model; + } + + static long double + unmarshal(variant const& value, marshaling_context*) + { + if (!value.is_number()) + throw std::runtime_error{ "not a number" }; + + return value.get_number< long double >(); + } +}; + +template<> +struct marshaling_traits< std::string > { + using model_type = std::string; + + static variant + marshal(std::string const& model, marshaling_context*) + { + return model; + } + + static std::string + unmarshal(variant const& value, marshaling_context*) + { + if (!value.is_string()) + throw std::runtime_error{ "not a string" }; + + return value.get_string(); + } +}; + +template< typename T > +struct marshaling_traits< std::vector< T > > { + using model_type = std::vector< T >; + + static variant + marshal(std::vector< T > const& model, marshaling_context* context) + { + std::vector< variant > a; + + for (auto const& j : model) + a.emplace_back(marshaling_traits< T >::marshal(j, context)); + + return a; + } + + static std::vector< T > + unmarshal(variant const& value, marshaling_context* context) + { + if (!value.is_array()) + throw std::runtime_error{ "not an array" }; + + std::vector< T > model; + + for (auto const& j : value) + model.emplace_back(marshaling_traits< T >::unmarshal(j, context)); + + return model; + } +}; + +template< typename T > +struct marshaling_traits< std::list< T > > { + using model_type = std::list< T >; + + static variant + marshal(std::list< T > const& model, marshaling_context* context) + { + std::vector< variant > a; + + for (auto const& j : model) + a.emplace_back(marshaling_traits< T >::marshal(j, context)); + + return a; + } + + static std::list< T > + unmarshal(variant const& value, marshaling_context* context) + { + if (!value.is_array()) + throw std::runtime_error{ "not an array" }; + + std::list< T > model; + + for (auto const& j : value) + model.emplace_back(marshaling_traits< T >::unmarshal(j, context)); + + return model; + } +}; + +template< typename T > +struct marshaling_traits< std::deque< T > > { + using model_type = std::deque< T >; + + static variant + marshal(std::deque< T > const& model, marshaling_context* context) + { + std::vector< variant > a; + + for (auto const& j : model) + a.emplace_back(marshaling_traits< T >::marshal(j, context)); + + return a; + } + + static std::deque< T > + unmarshal(variant const& value, marshaling_context* context) + { + if (!value.is_array()) + throw std::runtime_error{ "not an array" }; + + std::deque< T > model; + + for (auto const& j : value) + model.emplace_back(marshaling_traits< T >::unmarshal(j, context)); + + return model; + } +}; + +template< typename T > +struct marshaling_traits< std::set< T > > { + using model_type = std::set< T >; + + static variant + marshal(std::set< T > const& model, marshaling_context* context) + { + std::vector< variant > a; + + for (auto const& j : model) + a.emplace_back(marshaling_traits< T >::marshal(j, context)); + + return a; + } + + static std::set< T > + unmarshal(variant const& value, marshaling_context* context) + { + if (!value.is_array()) + throw std::runtime_error{ "not an array" }; + + std::set< T > model; + + for (auto const& j : value) + model.emplace(marshaling_traits< T >::unmarshal(j, context)); + + return model; + } +}; + +template<> +struct marshaling_traits< std::chrono::system_clock::time_point > { + using model_type = std::string; + + static variant + marshal(std::chrono::system_clock::time_point const& model, + marshaling_context*) + { + static constexpr const char time_format[] = "%a, %d %b %Y %H:%M:%S GMT"; + + std::time_t now_c = std::chrono::system_clock::to_time_t(model); + + struct std::tm tm_buf; + + std::stringstream str; + str.imbue(std::locale{}); + +#ifdef _MSC_VER + ::gmtime_s(&tm_buf, &now_c); // Stupid Microsoft. + str << std::put_time(&tm_buf, time_format); +#else + ::gmtime_r(&now_c, &tm_buf); + str << std::put_time(&tm_buf, time_format); +#endif + + return str.str(); + } + + static std::chrono::system_clock::time_point + unmarshal(variant const& value, marshaling_context*) + { + static constexpr const char time_format[] = "%a, %d %b %Y %H:%M:%S GMT"; + + if (!value.is_string()) + throw std::runtime_error{ "not a string" }; + + std::tm tm{}; + + std::istringstream str{ value.get_string() }; + str.imbue(std::locale{}); + + str >> std::get_time(&tm, time_format); + + if (str.fail()) + return {}; + +#ifdef _MSC_VER + auto localtime = _mkgmtime(&tm); + return std::chrono::system_clock::from_time_t(localtime); +#else + auto localtime = timegm(&tm); + return std::chrono::system_clock::from_time_t(localtime); +#endif + } +}; + +template +struct enum_mapping +{ + static + variant + marshal(E const& value, marshaling_context*) + { + return to_string(value); + } + + static + E + unmarshal(variant const& v, marshaling_context*) + { + return from_string(v.get_string()); + } +}; + +} // namespace code::json::marshaling + +#endif diff --git a/code/json/marshaling/marshaling-traits.test.cxx b/code/json/marshaling/marshaling-traits.test.cxx new file mode 100644 index 0000000..3621b59 --- /dev/null +++ b/code/json/marshaling/marshaling-traits.test.cxx @@ -0,0 +1,146 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include + +#define DEFINE_TEST(x) std::cout << x << '\n'; +#define TEST_TRUE(x) if (!(x)) return __LINE__; +#define TEST_FALSE(x) if ((x)) return __LINE__; +#define TEST_EQUAL(x, y) if ((x) != (y)) return __LINE__; + +int +main() +{ + DEFINE_TEST("optional{}") + { + using traits_type = code::json::marshaling::marshaling_traits< code::json::optional< int > >; + + std::optional< int > model; + + auto j = traits_type::marshal(model, nullptr); + + TEST_TRUE(j.is_undefined()); + } + + DEFINE_TEST("optional{0}") + { + using traits_type = code::json::marshaling::marshaling_traits< code::json::optional< int > >; + + std::optional< int > model{ 0 }; + + auto j = traits_type::marshal(model, nullptr); + + TEST_FALSE(j.is_undefined()); + TEST_TRUE(j.is_number()); + } + + DEFINE_TEST("string") + { + using traits_type = code::json::marshaling::marshaling_traits< std::string >; + + std::string model{ "hello, world" }; + + auto j = traits_type::marshal(model, nullptr); + + TEST_TRUE(j.is_string()); + TEST_EQUAL(j.get_string(), "hello, world"); + } + + DEFINE_TEST("vector{}") + { + using traits_type = code::json::marshaling::marshaling_traits< std::vector< int > >; + + std::vector< int > model1; + + auto j = traits_type::marshal(model1, nullptr); + + TEST_TRUE(j.is_array()); + + auto model2 = traits_type::unmarshal(j, nullptr); + + TEST_EQUAL(model1, model2); + } + + DEFINE_TEST("vector{...}") + { + using traits_type = code::json::marshaling::marshaling_traits< std::vector< int > >; + + std::vector< int > model1{ 1, 2, 3, 4 }; + + auto j = traits_type::marshal(model1, nullptr); + + TEST_TRUE(j.is_array()); + + auto model2 = traits_type::unmarshal(j, nullptr); + + TEST_EQUAL(model1, model2); + } + + DEFINE_TEST("list{}") + { + using traits_type = code::json::marshaling::marshaling_traits< std::list< int > >; + + std::list< int > model1; + + auto j = traits_type::marshal(model1, nullptr); + + TEST_TRUE(j.is_array()); + + auto model2 = traits_type::unmarshal(j, nullptr); + + TEST_EQUAL(model1, model2); + } + + DEFINE_TEST("list{...}") + { + using traits_type = code::json::marshaling::marshaling_traits< std::list< int > >; + + std::list< int > model1{ 1, 2, 3, 4 }; + + auto j = traits_type::marshal(model1, nullptr); + + TEST_TRUE(j.is_array()); + + auto model2 = traits_type::unmarshal(j, nullptr); + + TEST_EQUAL(model1, model2); + } + + DEFINE_TEST("deque{}") + { + using traits_type = code::json::marshaling::marshaling_traits< std::deque< int > >; + + std::deque< int > model1; + + auto j = traits_type::marshal(model1, nullptr); + + TEST_TRUE(j.is_array()); + + auto model2 = traits_type::unmarshal(j, nullptr); + + TEST_EQUAL(model1, model2); + } + + DEFINE_TEST("deque{...}") + { + using traits_type = code::json::marshaling::marshaling_traits< std::deque< int > >; + + std::deque< int > model1{ 1, 2, 3, 4 }; + + auto j = traits_type::marshal(model1, nullptr); + + TEST_TRUE(j.is_array()); + + auto model2 = traits_type::unmarshal(j, nullptr); + + TEST_EQUAL(model1, model2); + } + + return 0; +} diff --git a/code/json/marshaling/marshaling.hxx b/code/json/marshaling/marshaling.hxx new file mode 100644 index 0000000..ec8fb30 --- /dev/null +++ b/code/json/marshaling/marshaling.hxx @@ -0,0 +1,29 @@ +#ifndef code__json__marshaling__marshaling_hxx_ +#define code__json__marshaling__marshaling_hxx_ + +#include + +#include +#include + +#include + +namespace code::json::marshaling { + +template< typename T > +variant +marshal(T const& model, marshaling_context* context = nullptr) +{ + return marshaling_traits< T >::marshal(model, context); +} + +template< typename T > +typename marshaling_traits< T >::model_type +unmarshal(variant const& value, marshaling_context* context = nullptr) +{ + return marshaling_traits< T >::unmarshal(value, context); +} + +} // namespace code::json::marshaling + +#endif diff --git a/code/json/marshaling/serialize.hxx b/code/json/marshaling/serialize.hxx new file mode 100644 index 0000000..6237a47 --- /dev/null +++ b/code/json/marshaling/serialize.hxx @@ -0,0 +1,91 @@ +#ifndef code__json__marshaling__serialize_hxx_ +#define code__json__marshaling__serialize_hxx_ + +#include +#include + +#include +#include + +#include + +namespace code::json::marshaling { + +template< typename T > +void +serialize(std::ostream& o, T const& model) +{ + write(o, marshal(model)); +} + +template< typename T > +void +serialize(std::ostream&& o, T const& model) +{ + serialize(o, model); +} + +template< typename T > +std::string +serialize(T const& model) +{ + std::ostringstream str; + serialize(str, model); + return str.str(); +} + +template< typename T > +T +deserialize(diagnostics& d, + std::istream& i, + marshaling_context* context = nullptr) +{ + return unmarshal< T >(read(d, i), context); +} + +template< typename T > +T +deserialize(std::istream& i, marshaling_context* context = nullptr) +{ + return unmarshal< T >(read(i), context); +} + +template< typename T > +T +deserialize(diagnostics& d, + std::istream&& i, + marshaling_context* context = nullptr) +{ + return deserialize< T >(d, i, context); +} + +template< typename T > +T +deserialize(std::istream&& i, marshaling_context* context = nullptr) +{ + return deserialize< T >(i, context); +} + +template< typename T > +T +deserialize(diagnostics& d, + std::string const& str, + marshaling_context* context = nullptr) +{ + // TODO use std::string overload of read + return deserialize< T >( + d, std::istringstream{ str, std::ios::in | std::ios::binary }, context); +} + +template< typename T > +T +deserialize(std::string const& str, marshaling_context* context = nullptr) +{ + // TODO use std::string overload of read + return deserialize< T >( + std::istringstream{ str, std::ios::in | std::ios::binary }, context); +} + +} // namespace code::json::marshaling + +#endif diff --git a/code/json/marshaling/serialize.test.cxx b/code/json/marshaling/serialize.test.cxx new file mode 100644 index 0000000..a8f669b --- /dev/null +++ b/code/json/marshaling/serialize.test.cxx @@ -0,0 +1,60 @@ +#include +#include + +#include + +#define DEFINE_TEST(x) std::cout << x << '\n'; +#define TEST_TRUE(x) if (!(x)) return __LINE__; +#define TEST_FALSE(x) if ((x)) return __LINE__; +#define TEST_EQUAL(x, y) if ((x) != (y)) return __LINE__; + +struct person_name { + std::string first; + std::string last; + + static code::json::marshaling::mapping< person_name > const& + json() + { + static code::json::marshaling::mapping< person_name > const mapping{ + code::json::marshaling::member("first", &person_name::first), + code::json::marshaling::member("last", &person_name::last) + }; + + return mapping; + } +}; + +struct person { + person_name name; + int age; + + static code::json::marshaling::mapping< person > const& + json() + { + static code::json::marshaling::mapping< person > const mapping{ + code::json::marshaling::member("person", &person::name), + code::json::marshaling::member("age", &person::age) + }; + + return mapping; + } +}; + +int +main() +{ + DEFINE_TEST("round trip") + { + using namespace code::json::marshaling; + + person p1{ { "Jane", "Doe" }, 37 }; + + auto p2 = deserialize< person >(serialize(p1)); + + TEST_EQUAL(p1.name.first, p2.name.first); + TEST_EQUAL(p1.name.last, p2.name.last); + TEST_EQUAL(p1.age, p2.age); + } + + return 0; +} diff --git a/code/json/marshaling/traits.hxx b/code/json/marshaling/traits.hxx new file mode 100644 index 0000000..45d5a0b --- /dev/null +++ b/code/json/marshaling/traits.hxx @@ -0,0 +1,72 @@ +#ifndef code__json__marshaling__traits_hxx_ +#define code__json__marshaling__traits_hxx_ + +#include + +#include + +namespace code::json::marshaling { + +// is_optional. +// + +template< typename > +struct is_optional : std::false_type { +}; + +template< typename T > +struct is_optional< optional< T > > : std::true_type { +}; + +template< typename T > +constexpr bool is_optional_v = is_optional< T >::value; + +// function_traits. +// + +template< typename, typename = std::void_t< > > +struct function_traits; + +template< typename Ret, typename... Args > +struct function_traits< Ret(Args...) > { + static constexpr std::size_t arity = sizeof...(Args); + using return_type = std::decay_t< Ret >; + using argument_tuple = std::tuple< std::decay_t< Args >... >; +}; + +template< typename Ret, typename Class, typename... Args > +struct function_traits< Ret (Class::*)(Args...) > { + static constexpr std::size_t arity = sizeof...(Args); + using return_type = std::decay_t< Ret >; + using class_type = Class; + using argument_tuple = std::tuple< std::decay_t< Args >... >; +}; + +template< typename Ret, typename Class, typename... Args > +struct function_traits< Ret (Class::*)(Args...) const > { + static constexpr std::size_t arity = sizeof...(Args); + using return_type = std::decay_t< Ret >; + using class_type = Class; + using argument_tuple = std::tuple< std::decay_t< Args >... >; +}; + +template< typename Ret, typename... Args > +struct function_traits< Ret(*)(Args...) > { + static constexpr std::size_t arity = sizeof...(Args); + using return_type = std::decay_t< Ret >; + using argument_tuple = std::tuple< std::decay_t< Args >... >; +}; + +template< typename Callable > +struct function_traits< + Callable, + std::void_t< + decltype(&Callable::operator()) + > +> +: function_traits { +}; + +} // namespace code::json::marshaling + +#endif diff --git a/code/json/optional.hxx b/code/json/optional.hxx new file mode 100644 index 0000000..542d86b --- /dev/null +++ b/code/json/optional.hxx @@ -0,0 +1,30 @@ +#ifndef code__json__optional_hxx_ +#define code__json__optional_hxx_ + +/* + * This file will be REMOVED once is widely available. + */ + +#if !defined(__has_include) +# error "__has_include not supported" +#endif + +#if __has_include() +# include +#elif __has_include() +# include +#else +# error "No support for " +#endif + +namespace code::json { + +#if __has_include() +using std::optional; +#elif __has_include() +using std::experimental::optional; +#endif + +} // namespace code::json + +#endif diff --git a/code/json/parser.hxx b/code/json/parser.hxx new file mode 100644 index 0000000..0c06487 --- /dev/null +++ b/code/json/parser.hxx @@ -0,0 +1,758 @@ +#ifndef code__json__parser_hxx_ +#define code__json__parser_hxx_ + +#include +#include +#include +#include + +#include +#include + +#include // TODO remove +#include +#include +#include + +namespace code::json { + +class variant; + +class parser { +public: + parser() = default; + + template< typename InputIterator > + optional< variant > + try_parse(diagnostics& d, InputIterator& first, InputIterator last) + { + auto v = parse(d, first, last); + + if (d.errors().size() > 0) + return variant::undefined; + + return v; + } + + template< typename InputIterator > + optional< variant > + try_parse(diagnostics& d, InputIterator&& first, InputIterator last) + { + return try_parse(d, first, last); + } + + template< typename InputIterator > + variant + parse(diagnostics& d, InputIterator& first, InputIterator last) + { + skip_whitespace(first, last); + + auto v = try_parse_any(d, first, last); + + if (d.errors().size() > 0) + return variant::undefined; + + skip_whitespace(first, last); + + if (first != last) { + d.error(location(), "unexpected trailing data"); + return variant::undefined; + } + + return v; + } + + template< typename InputIterator > + variant + parse(diagnostics& d, InputIterator&& first, InputIterator last) + { + return parse(d, first, last); + } + +private: + template< typename InputIterator > + variant + try_parse_undefined(diagnostics& d, InputIterator& i, InputIterator last) + { + auto loc = location(); + + if (*i != 'n') { + d.error(location(), "expected 'n'"); + return { loc, variant::undefined }; + } + + advance(i, last); + + if (i == last || *i != 'u') { + d.error(location(), "expected 'u'"); + return { loc, variant::undefined }; + } + + advance(i, last); + + if (i == last || *i != 'l') { + d.error(location(), "expected 'l'"); + return { loc, variant::undefined }; + } + + advance(i, last); + + if (i == last || *i != 'l') { + d.error(location(), "expected 'l'"); + return { loc, variant::undefined }; + } + + advance(i, last); + + return { std::move(loc), variant::undefined }; + } + + template< typename InputIterator > + variant + try_parse_array(diagnostics& d, InputIterator& i, InputIterator last) + { + auto loc = location(); + + if (*i != '[') { + d.error(location(), "expected '['"); + return variant::undefined; + } + + advance(i, last); // skip '[' + + skip_whitespace(i, last); + + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + if (*i == ']') { + advance(i, last); + return variant{ std::move(loc), std::vector< variant >{} }; + } + + std::vector< variant > a; + + for (;;) { + auto v = try_parse_any(d, i, last); + + if (d.errors().size() > 0) + return variant::undefined; + + a.emplace_back(std::move(v)); + + skip_whitespace(i, last); + + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + if (',' != *i) + break; + + advance(i, last); + + skip_whitespace(i, last); + } + + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + if (*i != ']') { + d.error(location(), "expected ']'"); + return variant::undefined; + } + + advance(i, last); + + return variant{ std::move(loc), std::move(a) }; + } + + template< typename InputIterator > + variant + try_parse_boolean(diagnostics& d, InputIterator& i, InputIterator last) + { + auto loc = location(); + + if (*i == 't') { + advance(i, last); + + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + if (*i != 'r') { + d.error(location(), "unexpected character"); + return variant::undefined; + } + + advance(i, last); // skip 'r' + + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + if (*i != 'u') { + d.error(location(), "unexpected character"); + return variant::undefined; + } + + advance(i, last); // skip 'u' + + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + if (*i != 'e') { + d.error(location(), "unexpected character"); + return variant::undefined; + } + + advance(i, last); // skip 'e' + + return variant{ std::move(loc), true }; + } + + if (*i != 'f') + return {}; + + advance(i, last); + + if (i == last || *i != 'a') + return {}; + + advance(i, last); + + if (i == last || *i != 'l') + return {}; + + advance(i, last); + + if (i == last || *i != 's') + return {}; + + advance(i, last); + + if (i == last || *i != 'e') + return {}; + + advance(i, last); + + return variant{ std::move(loc), false }; + } + + template< typename InputIterator > + variant + try_parse_number(diagnostics& d, InputIterator& i, InputIterator last) + { + auto loc = location(); + + std::string buf; + buf.reserve(std::numeric_limits< long long >::digits10 * 2); + + bool is_signed{ false }; + + if (i != last && *i == '-') { + buf.push_back(*i++); + is_signed = true; + } + + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + // int + if (*i == '0') { + buf.push_back(*i++); + } + else if ('1' <= *i && *i <= '9') { + do { + buf.push_back(*i++); + } while (i != last && '0' <= *i && *i <= '9'); + } + else { + d.error(location(), "unexpected character"); + return variant::undefined; + } + + bool is_real{ false }; + + // frac + if (i != last && *i == '.') { + is_real = true; + buf.push_back(*i++); + + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + if (*i < '0' || '9' < *i) { + d.error(location(), "unexpected character"); + return variant::undefined; + } + + do { + buf.push_back(*i++); + } while (i != last && '0' <= *i && *i <= '9'); + } + + // exp + if (i != last && (*i == 'e' || *i == 'E')) { + is_real = true; + buf.push_back(*i++); + + if (i != last && (*i == '-' || *i == '+')) + buf.push_back(*i++); + + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + if (*i < '0' || '9' < *i) { + d.error(location(), "unexpected character"); + return variant::undefined; + } + + do { + buf.push_back(*i++); + } while (i != last && '0' <= *i && *i <= '9'); + } + + try { + if (is_real) { + return variant{ loc, std::stold(buf) }; + } + + if (is_signed) { + return variant{ loc, std::stoll(buf) }; + } + + return variant{ loc, std::stoull(buf) }; + } + catch (std::out_of_range const&) { + d.error(loc, "number too large"); + } + + return variant::undefined; + } + + template< typename InputIterator > + variant + try_parse_object(diagnostics& d, InputIterator& i, InputIterator last) + { + auto loc = location(); + + if (*i != '{') + return {}; + + advance(i, last); + + skip_whitespace(i, last); + + if (i == last) + return {}; + + if (*i == '}') { + advance(i, last); + return std::map< std::string, variant >{}; + } + + std::map< std::string, variant > o; + + for (;;) { + auto k = try_parse_string(d, i, last); + + if (d.errors().size() > 0) + return {}; + + skip_whitespace(i, last); + + if (i == last || *i != ':') + return {}; + + advance(i, last); + + skip_whitespace(i, last); + + if (i == last) { + d.error(location(), "unexpected end"); + return {}; + } + + auto v = try_parse_any(d, i, last); + + if (d.errors().size() > 0) + return {}; + + variant v_v{ std::move(v) }; + o.emplace(k.get_string(), std::move(v_v)); + + skip_whitespace(i, last); + + if (i == last) + return {}; + + if (',' != *i) + break; + advance(i, last); + + skip_whitespace(i, last); + } + + if (i == last || *i != '}') + return {}; + + advance(i, last); + + return variant{ std::move(loc), std::move(o) }; + } + + template< typename InputIterator > + variant + try_parse_string(diagnostics& d, InputIterator& i, InputIterator last) + { + auto loc = location(); + + std::string text; + + if (*i != '"') { + d.error(location(), "unexpected character"); + return variant::undefined; + } + + advance(i, last); + + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + while (i != last && *i != '"') { + if (*i == '\\') { // Parse escape sequence. + advance(i, last); // skip '\' + + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + switch (*i) { + case '"': + text += '\x22'; + advance(i, last); + break; + case '\\': + text += '\x5c'; + advance(i, last); + break; + case '/': + text += '\x2f'; + advance(i, last); + break; + case 'b': + text += '\x62'; + advance(i, last); + break; + case 'f': + text += '\x66'; + advance(i, last); + break; + case 'n': + text += '\x6e'; + advance(i, last); + break; + case 'r': + text += '\x72'; + advance(i, last); + break; + case 't': + text += '\x74'; + advance(i, last); + break; + case 'u': { + advance(i, last); // skip u + std::uint32_t u[4]; + + for (int index = 0; index < 4; ++index, advance(i, last)) { + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + std::uint32_t c = (unsigned char)*i; + + if (U'0' <= c && c <= U'9') { + u[index] = c - U'0'; + } + else if (U'a' <= c && c <= U'f') { + u[index] = (c - U'a') + 10; + } + else if (U'A' <= c && c <= U'F') { + u[index] = (c - U'A') + 10; + } + else { + d.error(location(), "expected digit"); + return variant::undefined; + } + } + + std::uint32_t utf32 = u[0] << 12 | u[1] << 8 | u[2] << 4 | u[3]; + + if (0xdc00 <= utf32 && utf32 <= 0xdfff) { + d.error(location(), "invalid unicode code point"); + return variant::undefined; + } + + if (0xd800 <= utf32 && utf32 <= 0xdbff) { + // We found a high surrogate, expect a following low-surrogate. + auto high = utf32; + + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + if (*i != '\\') { + d.error(location(), "unexpected character"); + return variant::undefined; + } + + advance(i, last); // skip backslash + + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + if (*i != 'u') { + d.error(location(), "unexpected character2"); + return variant::undefined; + } + + advance(i, last); // skip 'u' + + for (int index = 0; index < 4; ++index, advance(i, last)) { + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + std::uint32_t c = (unsigned char)*i; + + if (U'0' <= c && c <= U'9') { + u[index] = c - U'0'; + } + else if (U'a' <= c && c <= U'f') { + u[index] = (c - U'a') + 10; + } + else if (U'A' <= c && c <= U'F') { + u[index] = (c - U'A') + 10; + } + else { + d.error(location(), "expected digit"); + return {}; + } + } + + std::uint32_t low = u[0] << 12 | u[1] << 8 | u[2] << 4 | u[3]; + + utf32 = (high << 10) + low - 0x35fdc00; + } + + std::ostringstream str{ std::ios::out | std::ios::binary }; + + code::unicode::Utf8_encoder{}.encode(str, utf32); + text += str.str(); + + break; + } + } + + continue; + } + + if (is_control(i)) { + d.error(location(), "invalid character detected"); + return variant::undefined; + } + + text += *i++; + } + + if (i == last) { + d.error(location(), "premature end"); + return variant::undefined; + } + + if (*i != '"') { + d.error(location(), "unexpected character"); + return variant::undefined; + } + + advance(i, last); + + return variant{ std::move(loc), std::move(text) }; + } + + template< typename InputIterator > + variant + try_parse_any(diagnostics& d, InputIterator& i, InputIterator last) + { + auto loc = location(); + + if (i == last) { + d.error(loc, "premature end"); + return variant::undefined; + } + + switch (*i) { + case 'n': + return try_parse_undefined(d, i, last); + case '[': + return try_parse_array(d, i, last); + case 't': + case 'f': + return try_parse_boolean(d, i, last); + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return try_parse_number(d, i, last); + case '{': + return try_parse_object(d, i, last); + case '"': + return try_parse_string(d, i, last); + } + + std::ostringstream str; + str << "unexpected character '" << *i << "'"; + d.error(loc, str.str()); + + return variant::undefined; + } + + template< typename InputIterator > + void + skip_whitespace(InputIterator& i, InputIterator last) + { + while (i != last && is_whitespace< InputIterator >(i)) + advance(i, last); + } + + template< typename InputIterator > + bool + is_whitespace(InputIterator const& c) + { + switch (*c) { + case 0x09: // tab + case 0x0a: // lf + case 0x0d: // cr + case 0x20: // space + return true; + } + + return false; + } + + template< typename InputIterator > + bool + is_control(InputIterator const& c) + { + switch (*c) { + case '\x00': + case '\x01': + case '\x02': + case '\x03': + case '\x04': + case '\x05': + case '\x06': + case '\x07': + case '\x08': + case '\x09': + case '\x0A': + case '\x0B': + case '\x0C': + case '\x0D': + case '\x0E': + case '\x0F': + case '\x10': + case '\x11': + case '\x12': + case '\x13': + case '\x14': + case '\x15': + case '\x16': + case '\x17': + case '\x18': + case '\x19': + case '\x1A': + case '\x1B': + case '\x1C': + case '\x1D': + case '\x1E': + case '\x1F': + return true; + } + + return false; + } + + diagnostics::location + location() + { + return diagnostics::location{ line_, column_ }; + } + + template< typename InputIterator > + void + advance(InputIterator& i, InputIterator last) + { + if (i == last) + return; + + if (*i == '\r') { + ++line_; + column_ = 1; + ++i; + + if (i != last && *i == '\n') + ++i; + + return; + } + + if (*i == '\n') { + ++line_; + column_ = 1; + ++i; + return; + } + + ++column_; + ++i; + } + + std::uint32_t line_{ 1 }; + std::uint32_t column_{ 1 }; +}; + +} // namespace code::json + +#endif diff --git a/code/json/pointer.cxx b/code/json/pointer.cxx new file mode 100644 index 0000000..e0054c1 --- /dev/null +++ b/code/json/pointer.cxx @@ -0,0 +1,163 @@ +#include + +namespace code::json { + +pointer:: +pointer(std::string const& expression) + : refs_{parse_refs(expression)} +{} + +optional< variant > +pointer:: +read(variant const& source) const +{ + auto const* current = &source; + auto refs = refs_; + + while (refs.size() > 0) { + auto cref = refs.front(); + refs.pop(); + + if (current->is_object()) { + if (!current->contains(cref)) + return std::nullopt; + + current = ¤t->get(cref); + } + else if (current->is_array()) { + if (cref.size() == 1 && '~' == cref[0]) + throw invalid_array_index{ cref }; + + auto i = std::stoul(cref); + + if (current->size() <= i) + throw invalid_array_index{ cref }; + + current = ¤t->get(i); + } + else { + throw unexpected_type{}; + } + } + + return *current; +} + +void +pointer:: +write(variant& target, variant value) const +{ + if (!target.is_object()) + throw std::invalid_argument{"expected object"}; + + auto* current = ⌖ + auto refs = refs_; + + while (refs.size() > 1) { + auto cref = refs.front(); + refs.pop(); + + // Check what current is. + if (current->is_object()) { + if (current->contains(cref)) { + current = ¤t->get(cref); + } + else { + current->set(cref, std::map< std::string, variant >{}); + current = ¤t->get(cref); + } + } + else if (current->is_array()) { + if (cref.size() == 1 && '~' == cref[0]) + throw invalid_array_index{ cref }; + + auto i = std::stoul(cref); + + if (current->size() <= i) + throw invalid_array_index{ cref }; + + current = ¤t->get(i); + } + else { + throw unexpected_type{}; + } + } + + if (refs.empty()) + throw std::invalid_argument{"invalid argument"}; + + auto cref = refs.front(); + refs.pop(); + + if (current->is_object()) { + current->set(cref, value); + } + else if (current->is_array()) { + // TODO: Implement. + } + else { + throw unexpected_type{}; + } +} + +std::queue< std::string > +pointer:: +parse_refs(std::string const& expression) +{ + std::queue< std::string > refs; + + for (auto it = std::begin(expression); it != std::end(expression);) { + if ('/' != *it) + throw invalid_syntax{}; + + ++it; + + std::string cref; + + while (it != std::end(expression) && '/' != *it) { + switch (*it) { + case '~': + ++it; // skip tilde + if (it == std::end(expression)) + throw invalid_syntax{}; + else if ('0' == *it) + cref += '~'; + else if ('1' == *it) + cref += '/'; + else + throw invalid_syntax{}; + ++it; + break; + default: + cref += *it; + ++it; + break; + } + } + + if (!cref.empty()) + refs.push(std::move(cref)); + } + + return refs; +} + +std::string +to_string(pointer const& ptr) +{ + std::string rendered; + + auto refs = ptr.refs_; + + while (refs.size() > 0) { + auto cref = refs.front(); + refs.pop(); + + rendered += '/'; + rendered += cref; + } + + return rendered; +} + +} // namespace code::json diff --git a/code/json/pointer.hxx b/code/json/pointer.hxx new file mode 100644 index 0000000..4630890 --- /dev/null +++ b/code/json/pointer.hxx @@ -0,0 +1,41 @@ +#ifndef code__json__pointer_hxx_ +#define code__json__pointer_hxx_ + +#include +#include +#include +#include + +#include +#include + +namespace code::json { + +class pointer { +public: + explicit + pointer(std::string const& expression); + + optional< variant > + read(variant const& source) const; + + void + write(variant& target, variant value) const; + + friend + std::string + to_string(pointer const& ptr); + +private: + std::queue< std::string > + parse_refs(std::string const& expression); + + std::queue< std::string > refs_; +}; + +std::string +to_string(pointer const& ptr); + +} // namespace code::json + +#endif diff --git a/code/json/pointer.test.cxx b/code/json/pointer.test.cxx new file mode 100644 index 0000000..b803d5f --- /dev/null +++ b/code/json/pointer.test.cxx @@ -0,0 +1,95 @@ +#include + +#include +#include + +#define DEFINE_TEST(x) std::cout << x << '\n'; +#define TEST_TRUE(x) if (!(x)) return __LINE__; +#define TEST_EQUAL(x, y) if ((x) != (y)) return __LINE__; + +int +main() +{ + auto resolve = [](std::string const& data, char const* path) + { + code::json::pointer ptr{std::string{path}}; + return ptr.read(code::json::read(data)); + }; + + DEFINE_TEST("simple object") + { + std::string json = R"JSON( + { "haystack": "needle" } + )JSON"; + + auto variant = resolve(json, "/haystack"); + + TEST_TRUE(variant->is_string()); + TEST_EQUAL(variant->get_string(), "needle"); + } + + DEFINE_TEST("simple array") + { + std::string json = R"JSON( + [ "needle" ] + )JSON"; + + auto variant = resolve(json, "/0"); + + TEST_TRUE(variant->is_string()); + TEST_EQUAL(variant->get_string(), "needle"); + } + + DEFINE_TEST("complex") + { + std::string json = R"JSON( + { + "employees": [ + { + "name": "Doe, John", + "departments": ["HQ"], + "age": 37 + }, + { + "name": "Doe, Jane", + "departments": ["Software", "Finance"], + "age": 29 + } + ] + } + )JSON"; + + auto emp_0_name = resolve(json, "/employees/0/name"); + auto emp_0_department_0 = resolve(json, "/employees/0/departments/0"); + auto emp_0_age = resolve(json, "/employees/0/age"); + + auto emp_1_name = resolve(json, "/employees/1/name"); + auto emp_1_department_0 = resolve(json, "/employees/1/departments/0"); + auto emp_1_department_1 = resolve(json, "/employees/1/departments/1"); + auto emp_1_age = resolve(json, "/employees/1/age"); + + TEST_TRUE(emp_0_name->is_string()); + TEST_TRUE(emp_0_department_0->is_string()); + TEST_TRUE(emp_0_age->is_number()); + TEST_TRUE(emp_0_age->is_unsigned()); + + TEST_TRUE(emp_1_name->is_string()); + TEST_TRUE(emp_1_department_0->is_string()); + TEST_TRUE(emp_1_department_1->is_string()); + TEST_TRUE(emp_1_age->is_number()); + TEST_TRUE(emp_1_age->is_unsigned()); + + TEST_EQUAL(emp_0_name->get_string(), "Doe, John"); + TEST_EQUAL(emp_0_department_0->get_string(), "HQ"); + TEST_EQUAL(emp_0_age->get_number< signed int >(), 37); + TEST_EQUAL(emp_0_age->get_number< unsigned int >(), 37); + + TEST_EQUAL(emp_1_name->get_string(), "Doe, Jane"); + TEST_EQUAL(emp_1_department_0->get_string(), "Software"); + TEST_EQUAL(emp_1_department_1->get_string(), "Finance"); + TEST_EQUAL(emp_1_age->get_number< signed int >(), 29); + TEST_EQUAL(emp_1_age->get_number< unsigned int >(), 29); + } + + return 0; +} diff --git a/code/json/read.hxx b/code/json/read.hxx new file mode 100644 index 0000000..8bbc06b --- /dev/null +++ b/code/json/read.hxx @@ -0,0 +1,74 @@ +#ifndef code__json__read_hxx_ +#define code__json__read_hxx_ + +#include +#include +#include + +#include +#include + +namespace code::json { + +inline variant +read(std::string const& input) +{ + diagnostics d; + + auto first = input.begin(); + return parser{}.parse(d, first, input.end()); +} + +inline variant +read(diagnostics& d, std::string const& input) +{ + auto first = input.begin(); + return parser{}.parse(d, first, input.end()); +} + +inline optional< variant > +try_read(diagnostics& d, std::string const& input) +{ + auto first = input.begin(); + return parser{}.try_parse(d, first, input.end()); +} + +inline optional< variant > +try_read(std::string const& input) +{ + diagnostics d; + + auto first = input.begin(); + return parser{}.try_parse(d, first, input.end()); +} + +inline variant +read(diagnostics& d, std::istream& input) +{ + input.unsetf(std::istream::skipws); + + std::istreambuf_iterator< char > first{ input }; + return parser{}.parse(d, first, std::istreambuf_iterator< char >{}); +} + +inline variant +read(std::istream& input) +{ + diagnostics d; + return read(d, input); +} + +inline optional< variant > +try_read(std::istream& input) +{ + input.unsetf(std::istream::skipws); + + diagnostics d; + + std::istreambuf_iterator< char > first{ input }; + return parser{}.try_parse(d, first, std::istreambuf_iterator< char >{}); +} + +} // namespace code::json + +#endif diff --git a/code/json/read.test.cxx b/code/json/read.test.cxx new file mode 100644 index 0000000..c9f246e --- /dev/null +++ b/code/json/read.test.cxx @@ -0,0 +1,607 @@ +#include +#include + +#include + +#define DEFINE_TEST(x) std::cout << x << '\n'; +#define TEST_TRUE(x) if (!(x)) return __LINE__; +#define TEST_FALSE(x) if ((x)) return __LINE__; +#define TEST_EQUAL(x, y) if ((x) != (y)) return __LINE__; + +int +main() +{ + DEFINE_TEST("boolean: parse false") + { + std::string const text{ "false" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_boolean()); + TEST_EQUAL(var.get_boolean(), false); + TEST_EQUAL(var.location().line, 1); + TEST_EQUAL(var.location().column, 1); + } + + DEFINE_TEST("boolean: parse false, with whitespace") + { + std::string const text{ " false " }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_boolean()); + TEST_EQUAL(var.get_boolean(), false); + TEST_EQUAL(var.location().line, 1); + TEST_EQUAL(var.location().column, 2); + } + + DEFINE_TEST("boolean: parse true") + { + std::string const text{ "true" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_boolean()); + TEST_EQUAL(var.get_boolean(), true); + } + + DEFINE_TEST("boolean: parse true, with whitespace") + { + std::string const text{ " true " }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_boolean()); + TEST_EQUAL(var.get_boolean(), true); + TEST_EQUAL(var.location().line, 1); + TEST_EQUAL(var.location().column, 2); + } + + DEFINE_TEST("null: parse null") + { + std::string const text{ "null" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_undefined()); + TEST_EQUAL(var.location().line, 1); + TEST_EQUAL(var.location().column, 1); + } + + DEFINE_TEST("null: parse null, with whitespace") + { + std::string const text{ " null " }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_undefined()); + TEST_EQUAL(var.location().line, 1); + TEST_EQUAL(var.location().column, 3); + } + + DEFINE_TEST("array: parse empty") + { + std::string const text{ "[]" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_array()); + TEST_TRUE(var.empty()); + TEST_EQUAL(var.location().line, 1); + TEST_EQUAL(var.location().column, 1); + } + + DEFINE_TEST("array: parse empty, with whitespace") + { + std::string const text{ " [ ] " }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_array()); + TEST_TRUE(var.empty()); + TEST_EQUAL(var.location().line, 1); + TEST_EQUAL(var.location().column, 2); + } + + DEFINE_TEST("array: parse null element") + { + std::string const text{ "[null]" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_array()); + TEST_EQUAL(var.location().line, 1); + TEST_EQUAL(var.location().column, 1); + TEST_FALSE(var.empty()); + TEST_EQUAL(var.size(), 1); + TEST_TRUE(var.get(0).is_undefined()); + TEST_EQUAL(var.get(0).location().line, 1); + TEST_EQUAL(var.get(0).location().column, 2); + } + + DEFINE_TEST("array: parse null element, with whitespace") + { + std::string const text{ " [ null ] " }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_array()); + TEST_FALSE(var.empty()); + TEST_EQUAL(var.size(), 1); + + TEST_TRUE(var.get(0).is_undefined()); + TEST_EQUAL(var.get(0).location().line, 1); + TEST_EQUAL(var.get(0).location().column, 4); + } + + DEFINE_TEST("array: parse double null elements") + { + std::string const text{ "[null,null]" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_array()); + TEST_FALSE(var.empty()); + TEST_EQUAL(var.size(), 2); + + TEST_TRUE(var.get(0).is_undefined()); + TEST_EQUAL(var.get(0).location().line, 1); + TEST_EQUAL(var.get(0).location().column, 2); + + TEST_TRUE(var.get(1).is_undefined()); + TEST_EQUAL(var.get(1).location().line, 1); + TEST_EQUAL(var.get(1).location().column, 7); + } + + DEFINE_TEST("array: parse double null elements, with whitespace") + { + std::string const text{ " [ null , null ] " }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE( var.is_array()); + TEST_FALSE(var.empty()); + TEST_EQUAL(var.size(), 2); + + TEST_TRUE( var.get(0).is_undefined()); + TEST_EQUAL(var.get(0).location().line, 1); + TEST_EQUAL(var.get(0).location().column, 4); + + TEST_TRUE( var.get(1).is_undefined()); + TEST_EQUAL(var.get(1).location().line, 1); + TEST_EQUAL(var.get(1).location().column, 11); + } + + DEFINE_TEST("array: parse nested") + { + std::string const text{ " [ [ null , null ] , [ null ] ]" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_array()); + TEST_FALSE(var.empty()); + TEST_EQUAL(var.size(), 2); + + TEST_TRUE(var.get(0).is_array()); + TEST_FALSE(var.get(0).empty()); + TEST_EQUAL(var.get(0).size(), 2); + TEST_TRUE(var.get(0).get(0).is_undefined()); + TEST_TRUE(var.get(0).get(1).is_undefined()); + + TEST_TRUE(var. get(1).is_array()); + TEST_FALSE(var.get(1).empty()); + TEST_EQUAL(var.get(1).size(), 1); + TEST_TRUE(var. get(1).get(0).is_undefined()); + } + + DEFINE_TEST("object: parse empty") + { + std::string const text{ "{}" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_object()); + TEST_TRUE(var.keys().empty()); + } + + DEFINE_TEST("object: parse empty, with whitespace") + { + std::string const text{ " { } " }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_object()); + TEST_EQUAL(var.keys().size(), 0UL); + } + + DEFINE_TEST("object: parse 1-element object") + { + std::string const text{ "{\"key\":null}" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_object()); + TEST_EQUAL(var.keys().size(), 1UL); + + TEST_TRUE(var.get("key").is_undefined()); + } + + DEFINE_TEST("object: parse 1-element object (with whitespace)") + { + std::string const text{ " { \"key\" : null } " }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_object()); + TEST_EQUAL(var.keys().size(), 1UL); + + TEST_TRUE(var.get("key").is_undefined()); + } + + DEFINE_TEST("object: parse 2-element object") + { + std::string const text{ "{\"key1\":null,\"key2\":null}" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_object()); + TEST_EQUAL(var.keys().size(), 2UL); + + TEST_TRUE(var.get("key1").is_undefined()); + TEST_TRUE(var.get("key2").is_undefined()); + } + + DEFINE_TEST("object: parse 2-element object (with whitespace)") + { + std::string const text{ " { \"key1\" : null, \"key2\" : null } " }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_object()); + TEST_EQUAL(var.keys().size(), 2UL); + + TEST_TRUE(var.get("key1").is_undefined()); + TEST_TRUE(var.get("key2").is_undefined()); + } + + DEFINE_TEST("object: parse nested object") + { + std::string const text{ R"#({ + "k1": { + "k1": null + }, + "k2": { + "k1": null, + "k2": null + } + })#" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_object()); + TEST_EQUAL(var.keys().size(), 2UL); + + // Nested object 1. + auto object1 = var.get("k1"); + TEST_TRUE(object1.is_object()); + TEST_EQUAL(object1.keys().size(), 1UL); + TEST_TRUE(object1.get("k1").is_undefined()); + + // Nested object 2. + auto object2 = var.get("k2"); + TEST_TRUE(object2.is_object()); + TEST_EQUAL(object2.keys().size(), 2UL); + TEST_TRUE(object2.get("k1").is_undefined()); + TEST_TRUE(object2.get("k2").is_undefined()); + } + + DEFINE_TEST("number: integer: 0") + { + std::string const text{ "0" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_FALSE(var.is_signed()); + TEST_TRUE(var.is_unsigned()); + TEST_EQUAL(var.get_number< unsigned long long >(), 0); + } + + DEFINE_TEST("number: integer: 1") + { + std::string const text{ "1" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_FALSE(var.is_signed()); + TEST_TRUE(var.is_unsigned()); + TEST_EQUAL(var.get_number< unsigned long long >(), 1); + } + + DEFINE_TEST("number: integer: -1") + { + std::string const text{ "-1" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_signed()); + TEST_FALSE(var.is_unsigned()); + TEST_EQUAL(var.get_number< long long >(), -1); + } + + DEFINE_TEST("number: integer: 18446744073709551615") + { + std::string const text{ "18446744073709551615" }; + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_unsigned()); + TEST_FALSE(var.is_signed()); + TEST_EQUAL(var.get_number< unsigned long long >(), 18446744073709551615ULL); + } + + DEFINE_TEST("number: real: 0.0") + { + std::string const text{ "0.0" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_real()); + TEST_EQUAL(var.get_number< double >(), 0.0); + } + + DEFINE_TEST("number: real: 1.0") + { + std::string const text{ "1.0" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_real()); + TEST_EQUAL(var.get_number< double >(), 1.0); + } + + DEFINE_TEST("number: real: 1e3") + { + std::string const text{ "1e3" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_real()); + TEST_EQUAL(var.get_number< double >(), 1000.0); + } + + DEFINE_TEST("number: real: 1E3") + { + std::string const text{ "1E3" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_real()); + TEST_EQUAL(var.get_number< double >(), 1000.0); + } + + DEFINE_TEST("number: real: 1e+3") + { + std::string const text{ "1e+3" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_real()); + TEST_EQUAL(var.get_number< double >(), 1000.0); + } + + DEFINE_TEST("number: real: 1E+3") + { + std::string const text{ "1E+3" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_real()); + TEST_EQUAL(var.get_number< double >(), 1000.0); + } + + DEFINE_TEST("number: real: 1e-3") + { + std::string const text{ "1e-3" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_real()); + TEST_EQUAL(var.get_number< double >(), 0.001); + } + + DEFINE_TEST("number: real: 1E-3") + { + std::string const text{ "1E-3" }; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_real()); + TEST_EQUAL(var.get_number< double >(), 0.001); + } + + DEFINE_TEST("string: empty string") + { + code::json::diagnostics diag; + auto var = code::json::read(R"("")"); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_string()); + TEST_EQUAL(var.get_string(), ""); + } + + DEFINE_TEST("string: escape sequences") + { + std::pair< std::string, std::string > escapes[] = { + { R"("\"")", "\x22" }, // " (quotation mark) + { R"("\\")", "\x5C" }, // \ (reverse solidus) + { R"("\/")", "\x2F" }, // / (solidus) + { R"("\b")", "\x62" }, // b (backspace) + { R"("\f")", "\x66" }, // f (form feed) + { R"("\n")", "\x6E" }, // n (line feed) + { R"("\r")", "\x72" }, // r (carriage return) + { R"("\t")", "\x74" } // t (tab) + }; + + for (auto const& esc : escapes) { + code::json::diagnostics diag; + auto var = code::json::read(esc.first); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_string()); + TEST_EQUAL(var.get_string(), esc.second); + } + } + + DEFINE_TEST("string: utf-16 escape sequences") + { + std::pair< std::string, std::string > escapes[]{ + { R"("{\ud834\udd1e}")", "{\xf0\x9d\x84\x9e}" }, // MUSICAL SYMBOL G CLEF + }; + + for (auto const& escape : escapes) { + std::string text = escape.first; + + code::json::diagnostics diag; + auto var = code::json::read(diag, text); + + TEST_EQUAL(diag.errors().size(), 0); + TEST_EQUAL(diag.warnings().size(), 0); + + TEST_TRUE(var.is_string()); + TEST_EQUAL(var.get_string(), escape.second); + } + } + + return 0; +} diff --git a/code/json/resolve.test.cxx b/code/json/resolve.test.cxx new file mode 100644 index 0000000..913acf4 --- /dev/null +++ b/code/json/resolve.test.cxx @@ -0,0 +1,23 @@ +#include +#include + +#include +#include + +int +main(int argc, char* argv[]) +{ +#if 0 + std::stringstream str; + str << std::cin.rdbuf(); + + auto var = code::json::read(str.str()); + + std::cout << code::json::write(var); + + code::json::resolve(str.str(), argv[1]); + + + return 0; +#endif +} diff --git a/code/json/variant.hxx b/code/json/variant.hxx new file mode 100644 index 0000000..98d1db1 --- /dev/null +++ b/code/json/variant.hxx @@ -0,0 +1,285 @@ +#ifndef code__json__variant_hxx_ +#define code__json__variant_hxx_ + +#include +#include + +#include +#include +#include +#include +#include +#include + +class emitter; + +namespace code::json { + +//! Variant holding any valid JSON value. +class variant { +public: + //! Array iterator type. + using iterator = std::vector< variant >::iterator; + + //! Array iterator type. + using const_iterator = std::vector< variant >::const_iterator; + + //! Type used to denote an undefined variant. + struct undefined_t { }; + static constexpr undefined_t undefined{}; + + //! Construct an undefined variant. + variant(); + + //! Construct an undefined variant. + variant(undefined_t); + + //! Construct a variant holding a boolean value. + variant(bool value); + + //! Construct a variant holding a signed number. + variant(short int value); + + //! Construct a variant holding a signed number. + variant(int value); + + //! Construct a variant holding a signed number. + variant(long int value); + + //! Construct a variant holding a signed number. + variant(long long int value); + + //! Construct a variant holding an unsigned number. + variant(unsigned short int value); + + //! Construct a variant holding an unsigned number. + variant(unsigned int value); + + //! Construct a variant holding an unsigned number. + variant(unsigned long int value); + + //! Construct a variant holding an unsigned number. + variant(unsigned long long int value); + + //! Construct a variant holding a real number. + variant(float value); + + //! Construct a variant holding a real number. + variant(double value); + + //! Construct a variant holding a real number. + variant(long double value); + + //! Construct a variant holding a string. + variant(std::string value); + + //! Construct a variant holding a string. + variant(char const* value); + + //! Construct a variant holding an array. + variant(std::vector< variant > value); + + //! Construct a variant holding an object. + variant(std::map< std::string, variant > value); + + variant(diagnostics::location location, undefined_t); + + variant(diagnostics::location location, bool value); + + variant(diagnostics::location location, short int value); + + variant(diagnostics::location location, int value); + + variant(diagnostics::location location, long int value); + + variant(diagnostics::location location, long long int value); + + variant(diagnostics::location location, unsigned short int value); + + variant(diagnostics::location location, unsigned int value); + + variant(diagnostics::location location, unsigned long int value); + + variant(diagnostics::location location, unsigned long long int value); + + variant(diagnostics::location location, float value); + + variant(diagnostics::location location, double value); + + variant(diagnostics::location location, long double value); + + variant(diagnostics::location location, std::string value); + + variant(diagnostics::location location, char const* value); + + variant(diagnostics::location location, std::vector< variant > value); + + variant(diagnostics::location location, std::map< std::string, variant > value); + + //! Check if this variant is undefined. + bool + is_undefined() const; + + //! Check if this variant holds a boolean. + bool + is_boolean() const; + + //! Get boolean value. + bool + get_boolean() const; + + //! Check if this variant holds a number value. + bool + is_number() const; + + //! Check if this variant holds a signed value. + bool + is_signed() const; + + //! Check if this variant holds an unsigned value. + bool + is_unsigned() const; + + //! Check if this variant holds a real value. + bool + is_real() const; + + //! Get number value. + template< typename ArithmeticType > + ArithmeticType + get_number() const; + + //! Check if this variant holds a string. + bool + is_string() const; + + //! Get string value. + std::string const& + get_string() const; + + //! Check if this variant holds an array. + bool + is_array() const; + + //! Check if empty. + bool + empty() const; + + //! Get array size. + std::size_t + size() const; + + //! Get array iterator. + iterator + begin(); + + //! Get array iterator. + const_iterator + begin() const; + + //! Get array iterator. + const_iterator + cbegin() const; + + //! Get array iterator. + iterator + end(); + + //! Get array iterator. + const_iterator + end() const; + + //! Get array iterator. + const_iterator + cend() const; + + //! Get value at array index. + variant& + get(std::size_t index); + + //! Get value at array index. + variant const& + get(std::size_t index) const; + + //! Get value at array index. + variant const& + cget(std::size_t index) const; + + //! Push value to back of array. + void + push_back(variant v); + + //! Remove element from array. + void + erase(std::size_t index); + + //! Check if this variant holds an object. + bool + is_object() const; + + //! Check if object contains key. + bool + contains(std::string const& key) const; + + //! Get object keys. + std::set< std::string > + keys() const; + + //! Get object value. + variant& + get(std::string const& key); + + //! Get object value. + variant const& + get(std::string const& key) const; + + //! Get object value. + variant const& + cget(std::string const& key) const; + + //! Set object value. + void + set(std::string const& key, variant v); + + //! Remove element from object. + void + erase(std::string const& key); + + //! Get the location of this value. + diagnostics::location const& + location() const; + + template< typename Visitor > + friend void + visit(Visitor&& visitor, variant const& value) + { + std::visit(std::forward< Visitor >(visitor), value.value_); + } + + +private: + friend class emitter; + + //! Internal variant type. + using variant_type = std::variant< undefined_t, + bool, + long long int, + unsigned long long int, + long double, + std::string, + std::vector< variant >, + std::map< std::string, variant > >; + + //! Optional source location of this variant. + diagnostics::location location_; + + //! The value. + variant_type value_; +}; + +} // namespace code::json + +#include +#include + +#endif diff --git a/code/json/variant.ixx b/code/json/variant.ixx new file mode 100644 index 0000000..2a0552d --- /dev/null +++ b/code/json/variant.ixx @@ -0,0 +1,409 @@ +namespace code::json { + +inline variant::variant() : value_{ undefined_t{} } +{} + +inline variant::variant(undefined_t) : value_{ undefined_t{} } +{} + +inline variant::variant(bool value) : value_{ value } +{} + +inline variant::variant(short int value) + : value_{ static_cast< long long int >(value) } +{} + +inline variant::variant(int value) + : value_{ static_cast< long long int >(value) } +{} + +inline variant::variant(long int value) + : value_{ static_cast< long long int >(value) } +{} + +inline variant::variant(long long int value) : value_{ value } +{} + +inline variant::variant(unsigned short int value) + : value_{ static_cast< unsigned long long int >(value) } +{} + +inline variant::variant(unsigned int value) + : value_{ static_cast< unsigned long long int >(value) } +{} + +inline variant::variant(unsigned long int value) + : value_{ static_cast< unsigned long long int >(value) } +{} + +inline variant::variant(unsigned long long int value) : value_{ value } +{} + +inline variant::variant(float value) + : value_{ static_cast< long double >(value) } +{} + +inline variant::variant(double value) + : value_{ static_cast< long double >(value) } +{} + +inline variant::variant(long double value) : value_{ value } +{} + +inline variant::variant(std::string value) : value_{ value } +{} + +inline variant::variant(char const* value) : value_{ std::string{ value } } +{} + +inline variant::variant(std::vector< variant > value) : value_{ std::move(value) } +{} + +inline variant::variant(std::map< std::string, variant > value) : value_{ std::move(value) } +{} + +inline variant::variant(diagnostics::location location, undefined_t) + : location_{ std::move(location) }, value_{ undefined_t{} } +{} + +inline variant::variant(diagnostics::location location, bool value) + : location_{ std::move(location) }, value_{ value } +{} + +inline variant::variant(diagnostics::location location, short int value) + : location_{ std::move(location) }, + value_{ static_cast< long long int >(value) } +{} + +inline variant::variant(diagnostics::location location, int value) + : location_{ std::move(location) }, + value_{ static_cast< long long int >(value) } +{} + +inline variant::variant(diagnostics::location location, long int value) + : location_{ std::move(location) }, + value_{ static_cast< long long int >(value) } +{} + +inline variant::variant(diagnostics::location location, long long int value) + : location_{ std::move(location) }, value_{ value } +{} + +inline variant::variant(diagnostics::location location, + unsigned short int value) + : location_{ std::move(location) }, + value_{ static_cast< unsigned long long int >(value) } +{} + +inline variant::variant(diagnostics::location location, unsigned int value) + : location_{ std::move(location) }, + value_{ static_cast< unsigned long long int >(value) } +{} + +inline variant::variant(diagnostics::location location, unsigned long int value) + : location_{ std::move(location) }, + value_{ static_cast< unsigned long long int >(value) } +{} + +inline variant::variant(diagnostics::location location, + unsigned long long int value) + : location_{ std::move(location) }, value_{ value } +{} + +inline variant::variant(diagnostics::location location, float value) + : location_{ std::move(location) }, + value_{ static_cast< long double >(value) } +{} + +inline variant::variant(diagnostics::location location, double value) + : location_{ std::move(location) }, + value_{ static_cast< long double >(value) } +{} + +inline variant::variant(diagnostics::location location, long double value) + : location_{ std::move(location) }, value_{ value } +{} + +inline variant::variant(diagnostics::location location, std::string value) + : location_{ std::move(location) }, value_{ std::move(value) } +{} + +inline variant::variant(diagnostics::location location, char const* value) + : location_{ std::move(location) }, value_{ std::string{ value } } +{} + +inline variant::variant(diagnostics::location location, std::vector< variant > value) + : location_{ std::move(location) }, value_{ std::move(value) } +{} + +inline variant::variant(diagnostics::location location, std::map< std::string, variant > value) + : location_{ std::move(location) }, value_{ std::move(value) } +{} + +inline bool +variant::is_undefined() const +{ + return std::holds_alternative< undefined_t >(value_); +} + +inline bool +variant::is_boolean() const +{ + return std::holds_alternative< bool >(value_); +} + +inline bool +variant::get_boolean() const +{ + if (std::holds_alternative< bool >(value_)) + return std::get< bool >(value_); + + throw invalid_type{}; +} + +inline bool +variant::is_number() const +{ + return is_signed() || is_unsigned() || is_real(); +} + +inline bool +variant::is_signed() const +{ + return std::holds_alternative< long long int >(value_); +} + +inline bool +variant::is_unsigned() const +{ + return std::holds_alternative< unsigned long long int >(value_); +} + +inline bool +variant::is_real() const +{ + return std::holds_alternative< long double >(value_); +} + +inline bool +variant::is_string() const +{ + return std::holds_alternative< std::string >(value_); +} + +inline std::string const& +variant::get_string() const +{ + if (std::holds_alternative< std::string >(value_)) + return std::get< std::string >(value_); + + throw invalid_type{}; +} + +inline bool +variant::is_array() const +{ + return std::holds_alternative< std::vector< variant > >(value_); +} + +inline bool +variant::empty() const +{ + if (std::holds_alternative< std::vector< variant > >(value_)) + return std::get< std::vector< variant > >(value_).empty(); + + throw invalid_type{}; +} + +inline std::size_t +variant::size() const +{ + if (std::holds_alternative< std::vector< variant > >(value_)) + return std::get< std::vector< variant > >(value_).size(); + + throw invalid_type{}; +} + +inline variant::iterator +variant::begin() +{ + if (std::holds_alternative< std::vector< variant > >(value_)) + return std::get< std::vector< variant > >(value_).begin(); + + throw invalid_type{}; +} + +inline variant::const_iterator +variant::begin() const +{ + if (std::holds_alternative< std::vector< variant > >(value_)) + return std::get< std::vector< variant > >(value_).begin(); + + throw invalid_type{}; +} + +inline variant::const_iterator +variant::cbegin() const +{ + if (std::holds_alternative< std::vector< variant > >(value_)) + return std::get< std::vector< variant > >(value_).cbegin(); + + throw invalid_type{}; +} + +inline variant::iterator +variant::end() +{ + if (std::holds_alternative< std::vector< variant > >(value_)) + return std::get< std::vector< variant > >(value_).end(); + + throw invalid_type{}; +} + +inline variant::const_iterator +variant::end() const +{ + if (std::holds_alternative< std::vector< variant > >(value_)) + return std::get< std::vector< variant > >(value_).end(); + + throw invalid_type{}; +} + +inline variant::const_iterator +variant::cend() const +{ + if (std::holds_alternative< std::vector< variant > >(value_)) + return std::get< std::vector< variant > >(value_).cend(); + + throw invalid_type{}; +} + +inline variant& +variant::get(std::size_t index) +{ + if (std::holds_alternative< std::vector< variant > >(value_)) + return std::get< std::vector< variant > >(value_)[index]; + + throw invalid_type{}; +} + +inline variant const& +variant::get(std::size_t index) const +{ + if (std::holds_alternative< std::vector< variant > >(value_)) + return std::get< std::vector< variant > >(value_)[index]; + + throw invalid_type{}; +} + +inline variant const& +variant::cget(std::size_t index) const +{ + return get(index); +} + +inline void +variant::push_back(variant v) +{ + if (!std::holds_alternative< std::vector< variant > >(value_)) + throw invalid_type{}; + + std::get< std::vector< variant > >(value_).push_back(std::move(v)); +} + +inline void +variant::erase(std::size_t index) +{ + if (!std::holds_alternative< std::vector< variant > >(value_)) + throw invalid_type{}; + + auto& v = std::get< std::vector< variant > >(value_); + + if (index >= v.size()) + throw std::out_of_range{"index"}; + + v.erase(v.begin() + index); +} + +inline bool +variant::is_object() const +{ + return std::holds_alternative< std::map< std::string, variant > >(value_); +} + +inline bool +variant::contains(std::string const& key) const +{ + if (std::holds_alternative< std::map< std::string, variant > >(value_)) + return std::get< std::map< std::string, variant > >(value_).count(key) > 0; + + throw invalid_type{}; +} + +inline std::set< std::string > +variant::keys() const +{ + if (std::holds_alternative< std::map< std::string, variant > >(value_)) { + std::set< std::string > keys; + + for (auto const& j : std::get< std::map< std::string, variant > >(value_)) + keys.emplace(j.first); + + return keys; + } + + throw invalid_type{}; +} + +inline variant& +variant::get(std::string const& key) +{ + if (std::holds_alternative< std::map< std::string, variant > >(value_)) + return std::get< std::map< std::string, variant > >(value_).at(key); + + throw invalid_type{}; +} + +inline variant const& +variant::get(std::string const& key) const +{ + if (std::holds_alternative< std::map< std::string, variant > >(value_)) + return std::get< std::map< std::string, variant > >(value_).at(key); + + throw invalid_type{}; +} + +inline variant const& +variant::cget(std::string const& key) const +{ + if (std::holds_alternative< std::map< std::string, variant > >(value_)) + return std::get< std::map< std::string, variant > >(value_).at(key); + + throw invalid_type{}; +} + +inline void +variant::set(std::string const& key, variant v) +{ + if (!std::holds_alternative< std::map< std::string, variant > >(value_)) + throw invalid_type{}; + + std::get< std::map< std::string, variant > >(value_)[key] = std::move(v); +} + +inline void +variant::erase(std::string const& key) +{ + if (!std::holds_alternative< std::map< std::string, variant > >(value_)) + throw invalid_type{}; + + std::get< std::map< std::string, variant > >(value_).erase(key); +} + +inline diagnostics::location const& +variant::location() const +{ + return location_; +} + +} // namespace code::json diff --git a/code/json/variant.txx b/code/json/variant.txx new file mode 100644 index 0000000..9c850d3 --- /dev/null +++ b/code/json/variant.txx @@ -0,0 +1,19 @@ +namespace code::json { + +template< typename ArithmeticType > +ArithmeticType +variant::get_number() const +{ + if constexpr (std::is_arithmetic_v< ArithmeticType >) { + if (std::holds_alternative< long long int >(value_)) + return static_cast< ArithmeticType >(std::get< long long int >(value_)); + else if (std::holds_alternative< unsigned long long int >(value_)) + return static_cast< ArithmeticType >(std::get< unsigned long long int >(value_)); + else if (std::holds_alternative< long double >(value_)) + return static_cast< ArithmeticType >(std::get< long double >(value_)); + } + + throw invalid_type{}; +} + +} // namespace code::json diff --git a/code/json/version.hxx.in b/code/json/version.hxx.in new file mode 100644 index 0000000..f71c3f3 --- /dev/null +++ b/code/json/version.hxx.in @@ -0,0 +1,37 @@ +#ifndef code__json__version_hxx_ +#define code__json__version_hxx_ + +// The numeric version format is AAAAABBBBBCCCCCDDDE where: +// +// AAAAA - major version number +// BBBBB - minor version number +// CCCCC - bugfix version number +// DDD - alpha / beta (DDD + 500) version number +// E - final (0) / snapshot (1) +// +// When DDDE is not 0, 1 is subtracted from AAAAABBBBBCCCCC. For example: +// +// Version AAAAABBBBBCCCCCDDDE +// +// 0.1.0 0000000001000000000 +// 0.1.2 0000000001000020000 +// 1.2.3 0000100002000030000 +// 2.2.0-a.1 0000200001999990010 +// 3.0.0-b.2 0000299999999995020 +// 2.2.0-a.1.z 0000200001999990011 +// +#define LIBCODE_JSON_VERSION $libcode_json.version.project_number$ULL +#define LIBCODE_JSON_VERSION_STR "$libcode_json.version.project$" +#define LIBCODE_JSON_VERSION_ID "$libcode_json.version.project_id$" +#define LIBCODE_JSON_VERSION_FULL "$libcode_json.version$" + +#define LIBCODE_JSON_VERSION_MAJOR $libcode_json.version.major$ +#define LIBCODE_JSON_VERSION_MINOR $libcode_json.version.minor$ +#define LIBCODE_JSON_VERSION_PATCH $libcode_json.version.patch$ + +#define LIBCODE_JSON_PRE_RELEASE $libcode_json.version.pre_release$ + +#define LIBCODE_JSON_SNAPSHOT_SN $libcode_json.version.snapshot_sn$ULL +#define LIBCODE_JSON_SNAPSHOT_ID "$libcode_json.version.snapshot_id$" + +#endif diff --git a/code/json/write.hxx b/code/json/write.hxx new file mode 100644 index 0000000..7752db7 --- /dev/null +++ b/code/json/write.hxx @@ -0,0 +1,31 @@ +#ifndef code__json__write_hxx_ +#define code__json__write_hxx_ + +#include + +#include +#include +#include + +namespace code::json { + +class variant; + +inline void +write(std::ostream& os, variant const& v) +{ + visit(emitter{ os }, v); + os << '\n'; +} + +inline std::string +write(variant const& json) +{ + std::stringstream str; + write(str, json); + return str.str(); +} + +} // namespace code::json + +#endif diff --git a/code/json/write.test.cxx b/code/json/write.test.cxx new file mode 100644 index 0000000..fbbd16a --- /dev/null +++ b/code/json/write.test.cxx @@ -0,0 +1,31 @@ +#include +#include + +#include +#include + +#define DEFINE_TEST(x) std::cout << x << '\n'; +#define TEST_EQUAL(x, y) if ((x) != (y)) return __LINE__; + +int +main() +{ + DEFINE_TEST("write") + { + auto document = std::map< std::string, code::json::variant >{ + { { "title", "Coca-Cola Regular 1.5L" }, + { "gtin13", "5449000139306" } } + }; + + std::stringstream output; + code::json::write(output, document); + + TEST_EQUAL(output.str(), R"({ + "gtin13": "5449000139306", + "title": "Coca-Cola Regular 1.5L" +} +)"); + } + + return 0; +} diff --git a/manifest b/manifest new file mode 100644 index 0000000..e5ae7a6 --- /dev/null +++ b/manifest @@ -0,0 +1,13 @@ +: 1 +name: libcode-json +version: 0.1.0-a.0.z +language: c++ +summary: libcode-json C++ library +license: BSD-4-Clause +description-file: README.md +url: https://helloryan.se/code/ +email: ryan@helloryan.se +depends: * build2 >= 0.17.0 +depends: * bpkg >= 0.17.0 +depends: libcode-validation ^0.1.0- +depends: libcode-unicode ^0.1.0- diff --git a/repositories.manifest b/repositories.manifest new file mode 100644 index 0000000..594da43 --- /dev/null +++ b/repositories.manifest @@ -0,0 +1,10 @@ +: 1 +summary: libcode-uri project repository + +: +role: prerequisite +location: https://code.helloryan.se/code/libcode-validation.git##HEAD + +: +role: prerequisite +location: https://code.helloryan.se/code/libcode-unicode.git##HEAD diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..662178d --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,8 @@ +# Test executables. +# +driver + +# Testscript output directories (can be symlinks). +# +test +test-* diff --git a/tests/build/.gitignore b/tests/build/.gitignore new file mode 100644 index 0000000..974e01d --- /dev/null +++ b/tests/build/.gitignore @@ -0,0 +1,4 @@ +/config.build +/root/ +/bootstrap/ +build/ diff --git a/tests/build/bootstrap.build b/tests/build/bootstrap.build new file mode 100644 index 0000000..a07b5ea --- /dev/null +++ b/tests/build/bootstrap.build @@ -0,0 +1,5 @@ +project = # Unnamed tests subproject. + +using config +using test +using dist diff --git a/tests/build/root.build b/tests/build/root.build new file mode 100644 index 0000000..a67b2fe --- /dev/null +++ b/tests/build/root.build @@ -0,0 +1,16 @@ +cxx.std = latest + +using cxx + +hxx{*}: extension = hxx +ixx{*}: extension = ixx +txx{*}: extension = txx +cxx{*}: extension = cxx + +# Every exe{} in this subproject is by default a test. +# +exe{*}: test = true + +# The test target for cross-testing (running tests under Wine, etc). +# +test.target = $cxx.target diff --git a/tests/buildfile b/tests/buildfile new file mode 100644 index 0000000..aeeab15 --- /dev/null +++ b/tests/buildfile @@ -0,0 +1 @@ +./: {*/ -build/}