From 677eb1e1017630a1d1abbb528d28b90110990ef4 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Mon, 22 Jan 2024 12:02:41 +0200 Subject: Initial take --- libbuild2/json.cxx | 169 ++++++++++++++++++++++++++++ libbuild2/json.hxx | 233 +++++++++++++++++++++++++++++++++++++++ libbuild2/variable.cxx | 293 ++++++++++++++++++++++++++++++++++++++++++++++++- libbuild2/variable.hxx | 20 ++++ libbuild2/variable.ixx | 22 ++++ 5 files changed, 736 insertions(+), 1 deletion(-) create mode 100644 libbuild2/json.cxx create mode 100644 libbuild2/json.hxx diff --git a/libbuild2/json.cxx b/libbuild2/json.cxx new file mode 100644 index 0000000..0fee8af --- /dev/null +++ b/libbuild2/json.cxx @@ -0,0 +1,169 @@ +// file : libbuild2/json.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include + +#ifndef BUILD2_BOOTSTRAP +# include +# include +#endif + +using namespace butl::json; + +namespace build2 +{ +#ifndef BUILD2_BOOTSTRAP + json_value:: + json_value (parser& p) + { + // A JSON input text cannot be empty. + // + event e (*p.next ()); + + switch (e) + { + case event::begin_object: + { + container_type c; // For exception safety. + while (*p.next () != event::end_object) + { + string_type n (p.name ()); + json_value v (p); + v.name = move (n); + c.push_back (move (v)); + } + + new (&container) container_type (move (c)); + type = json_type::object; + break; + } + case event::begin_array: + { + container_type c; // For exception safety. + while (*p.peek () != event::end_array) + c.push_back (json_value (p)); + p.next (); // Consume end_array. + + new (&container) container_type (move (c)); + type = json_type::array; + break; + } + case event::string: + { + string_type& s (p.value ()); + + // Don't move if small string optimized. + // + if (s.size () > 15) + new (&string) string_type (move (s)); + else + new (&string) string_type (s); + + type = json_type::string; + break; + } + case event::number: + { + string_type& s (p.value ()); + + if (s[0] == '-') + { + signed_number = p.value (); + type = json_type::signed_number; + } + else + { + unsigned_number = p.value (); + type = json_type::unsigned_number; + } + + break; + } + case event::boolean: + { + boolean = p.value (); + type = json_type::boolean; + break; + } + case event::null: + { + type = json_type::null; + break; + } + case event::name: + case event::end_array: + case event::end_object: + { + assert (false); + type = json_type::null; + break; + } + } + } + + void + serialize (buffer_serializer& s, const json_value& v) + { + if (v.name) + s.member_name (*v.name); + + switch (v.type) + { + case json_type::null: + { + s.value (nullptr); + break; + } + case json_type::boolean: + { + s.value (v.boolean); + break; + } + case json_type::signed_number: + { + s.value (v.signed_number); + break; + } + case json_type::unsigned_number: + { + s.value (v.unsigned_number); + break; + } + case json_type::string: + { + s.value (v.string); + break; + } + case json_type::array: + { + s.begin_array (); + for (const json_value& e: v.container) + serialize (s, e); + s.end_array (); + break; + } + case json_type::object: + { + s.begin_object (); + for (const json_value& m: v.container) + serialize (s, m); + s.end_object (); + break; + } + } + } +#else + json_value:: + json_value (parser&) + { + assert (false); + type = json_type::null; + } + + void + serialize (buffer_serializer&, const json_value&) + { + assert (false); + } +#endif +} diff --git a/libbuild2/json.hxx b/libbuild2/json.hxx new file mode 100644 index 0000000..816b55c --- /dev/null +++ b/libbuild2/json.hxx @@ -0,0 +1,233 @@ +// file : libbuild2/json.hxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#ifndef LIBBUILD2_JSON_HXX +#define LIBBUILD2_JSON_HXX + +#include +#include + +#include + +namespace butl +{ + namespace json + { + class parser; + class buffer_serializer; + } +} + +namespace build2 +{ + // This JSON representation has two extensions compared to the standard JSON + // model: it distinguishes between signed and unsigned numbers and + // represents an object member as a JSON value rather than, say, a pair of a + // string and value. The latter allows us to use the JSON value itself as an + // element of a container. + // + enum class json_type + { + null, + boolean, + signed_number, + unsigned_number, + string, + array, + object, + }; + + class LIBBUILD2_SYMEXPORT json_value + { + public: + using string_type = build2::string; + using container_type = vector; + + json_type type; + + optional name; // If present, then this is a member with value. + + union + { + bool boolean; + int64_t signed_number; + uint64_t unsigned_number; + string_type string; + container_type container; // arrary and object + }; + + // Throws invalid_json_input. + // + explicit + json_value (butl::json::parser&); + + explicit + json_value (json_type t = json_type::null) + : type (t) + { + switch (type) + { + case json_type::null: break; + case json_type::boolean: boolean = false; break; + case json_type::signed_number: signed_number = 0; break; + case json_type::unsigned_number: unsigned_number = 0; break; + case json_type::string: new (&string) string_type (); break; + case json_type::array: + case json_type::object: new (&container) container_type (); break; + } + } + + json_value (string_type member_name, json_type t) + : json_value (t) {name = move (member_name);} + + explicit + json_value (std::nullptr_t) + : type (json_type::null) {} + + json_value (string_type member_name, std::nullptr_t v) + : json_value (v) {name = move (member_name);} + + explicit + json_value (bool v) + : type (json_type::boolean), boolean (v) {} + + json_value (string_type member_name, bool v) + : json_value (v) {name = move (member_name);} + + explicit + json_value (int64_t v) + : type (json_type::signed_number), signed_number (v) {} + + json_value (string_type member_name, int64_t v) + : json_value (v) {name = move (member_name);} + + explicit + json_value (uint64_t v) + : type (json_type::unsigned_number), unsigned_number (v) {} + + json_value (string_type member_name, uint64_t v) + : json_value (v) {name = move (member_name);} + + explicit + json_value (string_type v) + : type (json_type::string), string (move (v)) {} + + json_value (string_type member_name, string_type v) + : json_value (move (v)) {name = move (member_name);} + + explicit + json_value (container_type v, json_type t) + : type (t), container (move (v)) + { +#ifndef NDEBUG + assert (t == json_type::array || t == json_type::object); + + for (const json_value& e: container) + assert (e.name.has_value () == (t == json_type::object)); +#endif + } + + json_value (string_type member_name, container_type v, json_type t) + : json_value (move (v), t) {name = move (member_name);} + + // Note that the moved-from value becomes null. + // + json_value (json_value&& v) noexcept + : type (v.type), name (move (v.name)) + { + switch (type) + { + case json_type::null: + break; + case json_type::boolean: + boolean = v.boolean; + break; + case json_type::signed_number: + signed_number = v.signed_number; + break; + case json_type::unsigned_number: + unsigned_number = v.unsigned_number; + break; + case json_type::string: + new (&string) string_type (move (v.string)); + v.string.~string_type (); + break; + case json_type::array: + case json_type::object: + new (&container) container_type (move (v.container)); + v.container.~container_type (); + break; + } + + v.type = json_type::null; + v.name = nullopt; + } + + json_value& operator= (json_value&& v) noexcept + { + if (this != &v) + { + this->~json_value (); + new (this) json_value (move (v)); + } + return *this; + } + + json_value (const json_value& v) + : type (v.type), name (v.name) + { + switch (type) + { + case json_type::null: + break; + case json_type::boolean: + boolean = v.boolean; + break; + case json_type::signed_number: + signed_number = v.signed_number; + break; + case json_type::unsigned_number: + unsigned_number = v.unsigned_number; + break; + case json_type::string: + new (&string) string_type (v.string); + break; + case json_type::array: + case json_type::object: + new (&container) container_type (v.container); + break; + } + } + + json_value& operator= (const json_value& v) + { + if (this != &v) + { + this->~json_value (); + new (this) json_value (v); + } + return *this; + } + + ~json_value () noexcept + { + switch (type) + { + case json_type::null: + case json_type::boolean: + case json_type::signed_number: + case json_type::unsigned_number: break; + case json_type::string: string.~string_type (); break; + case json_type::array: + case json_type::object: container.~container_type (); break; + } + } + }; + + // Throws invalid_json_output. + // + LIBBUILD2_SYMEXPORT void + serialize (butl::json::buffer_serializer&, const json_value&); +} + +#endif // LIBBUILD2_JSON_HXX diff --git a/libbuild2/variable.cxx b/libbuild2/variable.cxx index 392c9bb..62da025 100644 --- a/libbuild2/variable.cxx +++ b/libbuild2/variable.cxx @@ -7,6 +7,11 @@ #include +#ifndef BUILD2_BOOTSTRAP +# include +# include +#endif + #include #include @@ -460,7 +465,7 @@ namespace build2 m += "name '" + to_string (n) + '\''; } - throw invalid_argument (m); + throw invalid_argument (move (m)); } // names @@ -1475,6 +1480,292 @@ namespace build2 &default_empty }; + // json + // + json_value value_traits:: + convert (names&& ns) + { + auto to_string_value = [] (name& n, const char* what) -> string + { + if (n.typed () || n.qualified () || n.pattern) + throw_invalid_argument (n, nullptr, what); + + string s; + + if (n.simple ()) + s.swap (n.value); + else + { + // Note that here we cannot assume what's in dir is really a path + // (think s/foo/bar/) so we have to reverse it exactly. + // + s = move (n.dir).representation (); // Move out of path. + + if (!n.value.empty ()) + s += n.value; // Separator is already there. + } + + return s; + }; + + auto to_json_value = [] (name& n, const char* what) -> json_value + { + if (n.typed () || n.qualified () || n.pattern) + throw_invalid_argument (n, nullptr, what); + + string s; + + if (n.simple ()) + s.swap (n.value); + else + { + // Note that here we cannot assume what's in dir is really a path + // (think s/foo/bar/) so we have to reverse it exactly. + // + s = move (n.dir).representation (); // Move out of path. + + if (!n.value.empty ()) + s += n.value; // Separator is already there. + + // A path is always interpreted as a JSON string. + // + return json_value (move (s)); + } + + bool f; + if (s == "null") + return json_value (); + else if ((f = (s == "true")) || s == "false") + return json_value (f); + else if (s.find_first_not_of ( + "0123456789", (f = (s[0] == '-')) ? 1 : 0) == string::npos) + { + name n (move (s)); + return f + ? json_value (value_traits::convert (n, nullptr)) + : json_value (value_traits::convert (n, nullptr)); + } + else + { + // If this is not already a JSON string, array, or object, treat it as + // a string. + // + if (s[0] != '"') + { + // While the quote character must be first, `{` and `[` could be + // preceded with whitespaces. + // + size_t p (s.find_first_not_of (" \t\n\r")); + + if (p == string::npos || (s[p] != '{' && s[p] != '[')) + { + return json_value (move (s)); + } + } + + // Parse as valid JSON input. + // +#ifndef BUILD2_BOOTSTRAP + using namespace butl::json; + + try + { + parser p (s, empty_string /* name */); + return json_value (p); + } + catch (const invalid_json_input& e) + { + // @@ How can we communicate value, line/column, position? + + string m ("invalid json value: "); + m += e.what (); + + throw invalid_argument (move (m)); + } +#else + throw invalid_argument ("json parsing requested during bootstrap"); +#endif + } + }; + + size_t n (ns.size ()); + + if (n == 0) + { + return json_value (); // null + } + else if (n == 1) + { + return to_json_value (ns.front (), "json"); + } + else + { + if (ns.front ().pair) // object + { + json_value r (json_type::object); + r.container.reserve (n / 2); + + for (auto i (ns.begin ()); i != ns.end (); ++i) + { + if (!i->pair) + throw invalid_argument ( + "pair expected in json member value '" + to_string (*i) + '\''); + + // @@ Should we handle quoted member names? + // + string m (to_string_value (*i, "json member name")); + json_value v (to_json_value (*++i, "json member")); + v.name = move (m); + + // @@ Override duplicates or fail? + // + r.container.push_back (move (v)); + } + + return r; + } + else // array + { + json_value r (json_type::array); + r.container.reserve (n); + + for (name& n: ns) + r.container.push_back (to_json_value (n, "json array element")); + + return r; + } + } + } + + void value_traits:: + assign (value& v, json_value&& x) + { + if (v) + v.as () = move (x); + else + new (&v.data_) json_value (move (x)); + } + + void value_traits:: + append (value& v, json_value&& x) + { + if (v) + { + json_value& p (v.as ()); + + if (p.empty ()) + p.swap (x); + else + p.insert (p.end (), + make_move_iterator (x.begin ()), + make_move_iterator (x.end ())); + } + else + new (&v.data_) json_value (move (x)); + } + + void value_traits:: + prepend (value& v, json_value&& x) + { + if (v) + { + json_value& p (v.as ()); + + if (!p.empty ()) + x.insert (x.end (), + make_move_iterator (p.begin ()), + make_move_iterator (p.end ())); + + p.swap (x); + } + else + new (&v.data_) json_value (move (x)); + } + + void + json_value_assign (value& v, names&& ns, const variable*) + { + if (!v) + { + new (&v.data_) json_value (); + v.null = false; + } + + v.as ().assign (make_move_iterator (ns.begin ()), + make_move_iterator (ns.end ())); + } + + void + json_value_append (value& v, names&& ns, const variable*) + { + if (!v) + { + new (&v.data_) json_value (); + v.null = false; + } + + auto& x (v.as ()); + x.insert (x.end (), + make_move_iterator (ns.begin ()), + make_move_iterator (ns.end ())); + } + + void + json_value_prepend (value& v, names&& ns, const variable*) + { + if (!v) + { + new (&v.data_) json_value (); + v.null = false; + } + + auto& x (v.as ()); + x.insert (x.begin (), + make_move_iterator (ns.begin ()), + make_move_iterator (ns.end ())); + } + + static names_view + json_value_reverse (const value& v, names&, bool) + { + const auto& x (v.as ()); + return names_view (x.data (), x.size ()); + } + + static int + json_value_compare (const value& l, const value& r) + { + return vector_compare (l, r); + } + + const json_value value_traits::empty_instance; + + const char* const value_traits::type_name = "json"; + + // Note that whether the json value is a container or not depends on its + // payload type. However, for our purposes it feels correct to assume it is + // a container rather than not with itself as the element type (see + // value_traits::{container, element_type} usage for details). + // + const value_type value_traits::value_type + { + type_name, + sizeof (json_value), + nullptr, // No base. + true, // Container. + &value_traits::value_type, // Element (itself). + &default_dtor, + &default_copy_ctor, + &default_copy_assign, + &cmdline_assign, + &cmdline_append, + &cmdline_prepend, + &cmdline_reverse, + nullptr, // No cast (cast data_ directly). + &cmdline_compare, + &default_empty + }; + // cmdline // cmdline value_traits:: diff --git a/libbuild2/variable.hxx b/libbuild2/variable.hxx index a91a7e0..eddf1ac 100644 --- a/libbuild2/variable.hxx +++ b/libbuild2/variable.hxx @@ -15,6 +15,8 @@ #include #include +#include + #include #include #include @@ -1179,6 +1181,24 @@ namespace build2 static const map_value_type value_type; }; + // json + // + template <> + struct LIBBUILD2_SYMEXPORT value_traits + { + static_assert (sizeof (json_value) <= value::size_, "insufficient space"); + + static json_value convert (names&&); + static void assign (value&, json_value&&); + static void append (value&, json_value&&); + static void prepend (value&, json_value&&); + static bool empty (const json_value&); // null or empty string|array|object + + static const json_value empty_instance; // null + static const char* const type_name; + static const build2::value_type value_type; + }; + // Canned command line to be re-lexed (used in {Build,Test}scripts). // // Note that because the executable can be specific as a target or as diff --git a/libbuild2/variable.ixx b/libbuild2/variable.ixx index 51c35fd..4a0176f 100644 --- a/libbuild2/variable.ixx +++ b/libbuild2/variable.ixx @@ -906,6 +906,28 @@ namespace build2 new (&v.data_) map (move (x)); } + // json + // + inline bool value_traits:: + empty (const json_value& v) + { + if (!v.name) + { + switch (v.type) + { + case json_type::null: return true; + case json_type::boolean: + case json_type::signed_number: + case json_type::unsigned_number: break; + case json_type::string: return v.string.empty (); + case json_type::array: + case json_type::object: return v.container.empty (); + } + } + + return false; + } + // variable_pool // inline const variable* variable_pool:: -- cgit v1.1