aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2024-01-22 12:02:41 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2024-01-22 12:02:41 +0200
commit677eb1e1017630a1d1abbb528d28b90110990ef4 (patch)
tree2926def456a6008f54b6d8d2936fd12ce135db95
parentbcde39204f0f15f207a10da59347db514936c617 (diff)
Initial take
-rw-r--r--libbuild2/json.cxx169
-rw-r--r--libbuild2/json.hxx233
-rw-r--r--libbuild2/variable.cxx293
-rw-r--r--libbuild2/variable.hxx20
-rw-r--r--libbuild2/variable.ixx22
5 files changed, 736 insertions, 1 deletions
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 <libbuild2/json.hxx>
+
+#ifndef BUILD2_BOOTSTRAP
+# include <libbutl/json/parser.hxx>
+# include <libbutl/json/serializer.hxx>
+#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<int64_t> ();
+ type = json_type::signed_number;
+ }
+ else
+ {
+ unsigned_number = p.value<uint64_t> ();
+ type = json_type::unsigned_number;
+ }
+
+ break;
+ }
+ case event::boolean:
+ {
+ boolean = p.value<bool> ();
+ 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 <libbuild2/types.hxx>
+#include <libbuild2/utility.hxx>
+
+#include <libbuild2/export.hxx>
+
+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_value>;
+
+ json_type type;
+
+ optional<string_type> 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 <libbutl/path-pattern.hxx>
+#ifndef BUILD2_BOOTSTRAP
+# include <libbutl/json/parser.hxx>
+# include <libbutl/json/serializer.hxx>
+#endif
+
#include <libbuild2/target.hxx>
#include <libbuild2/diagnostics.hxx>
@@ -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<project_name>
};
+ // json
+ //
+ json_value value_traits<json_value>::
+ 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<int64_t>::convert (n, nullptr))
+ : json_value (value_traits<uint64_t>::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<json_value>::
+ assign (value& v, json_value&& x)
+ {
+ if (v)
+ v.as<json_value> () = move (x);
+ else
+ new (&v.data_) json_value (move (x));
+ }
+
+ void value_traits<json_value>::
+ append (value& v, json_value&& x)
+ {
+ if (v)
+ {
+ json_value& p (v.as<json_value> ());
+
+ 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<json_value>::
+ prepend (value& v, json_value&& x)
+ {
+ if (v)
+ {
+ json_value& p (v.as<json_value> ());
+
+ 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<json_value> ().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<json_value> ());
+ 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<json_value> ());
+ 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<json_value> ());
+ return names_view (x.data (), x.size ());
+ }
+
+ static int
+ json_value_compare (const value& l, const value& r)
+ {
+ return vector_compare<name> (l, r);
+ }
+
+ const json_value value_traits<json_value>::empty_instance;
+
+ const char* const value_traits<json_value>::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<json_value>::value_type
+ {
+ type_name,
+ sizeof (json_value),
+ nullptr, // No base.
+ true, // Container.
+ &value_traits<json_value>::value_type, // Element (itself).
+ &default_dtor<json_value>,
+ &default_copy_ctor<json_value>,
+ &default_copy_assign<json_value>,
+ &cmdline_assign,
+ &cmdline_append,
+ &cmdline_prepend,
+ &cmdline_reverse,
+ nullptr, // No cast (cast data_ directly).
+ &cmdline_compare,
+ &default_empty<cmdline>
+ };
+
// cmdline
//
cmdline value_traits<cmdline>::
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 <libbuild2/forward.hxx>
#include <libbuild2/utility.hxx>
+#include <libbuild2/json.hxx>
+
#include <libbuild2/context.hxx>
#include <libbuild2/target-type.hxx>
#include <libbuild2/diagnostics.hxx>
@@ -1179,6 +1181,24 @@ namespace build2
static const map_value_type<K, V> value_type;
};
+ // json
+ //
+ template <>
+ struct LIBBUILD2_SYMEXPORT value_traits<json_value>
+ {
+ 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<K, V> (move (x));
}
+ // json
+ //
+ inline bool value_traits<json_value>::
+ 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::