diff options
author | Boris Kolpackov <boris@codesynthesis.com> | 2024-02-06 05:22:12 +0200 |
---|---|---|
committer | Boris Kolpackov <boris@codesynthesis.com> | 2024-02-07 15:02:38 +0200 |
commit | 36d6b4e5549dc45baf890105de5ef487211f0144 (patch) | |
tree | 762f9eba621026e9bb7d8fd69107a4447783a45a | |
parent | a5acaba537dab8e06be1197916acff86699aa5a3 (diff) |
Add experimental support for JSON value types
New types:
json
json_array
json_object
New functions:
$json.value_type(<json>)
$json.value_size(<json>)
$json.member_{name,value}(<json-member>)
$json.object_names(<json-object>)
$json.array_size(<json-array>)
$json.array_find(<json-array>, <json>)
$json.array_find_index(<json-array>, <json>)
$json.load(<path>)
$json.parse(<text>)
$json.serialize(<json>[, <indentation>])
For example, to load a JSON value from a file:
j = $json.load($src_base/board.json)
Or to construct it in a buildfile:
j = [json] one@1 two@([json] 2 3 4) three@([json] x@1 y@-1)
This can also be done incrementally with append/prepend:
j = [json_object]
j += one@1
j += two@([json] 2 3 4)
j += three@([json] x@1 y@-1)
Instead of using this JSON-like syntax, one can also specify valid JSON
input text:
j = [json] '{"one":1, "two":[2, 3, 4], "three":{"x":1, "y":-1}'
Besides the above set of functions, other handy ways to access components
in a JSON value are iteration and subscript. For example:
for m: $j
print $member_name($m) $member_value($m)
print ($j[three])
A subscript can be nested:
print ($j[two][1])
print ($j[three][x])
While a JSON value can be printed directly like any other value, the
representation will not be pretty-printed. As a result, for complex
JSON values, printing a serialized representation might be a more
readable option:
info $serialize($j)
-rw-r--r-- | doc/buildfile | 1 | ||||
-rw-r--r-- | doc/manual.cli | 23 | ||||
-rw-r--r-- | libbuild2/function.cxx | 3 | ||||
-rw-r--r-- | libbuild2/functions-json.cxx | 240 | ||||
-rw-r--r-- | libbuild2/json.cxx | 856 | ||||
-rw-r--r-- | libbuild2/json.hxx | 316 | ||||
-rw-r--r-- | libbuild2/json.ixx | 222 | ||||
-rw-r--r-- | libbuild2/parser.cxx | 124 | ||||
-rw-r--r-- | libbuild2/variable.cxx | 926 | ||||
-rw-r--r-- | libbuild2/variable.hxx | 61 | ||||
-rw-r--r-- | libbuild2/variable.ixx | 107 | ||||
-rw-r--r-- | tests/function/builtin/testscript | 3 | ||||
-rw-r--r-- | tests/function/json/buildfile | 4 | ||||
-rw-r--r-- | tests/function/json/testscript | 224 | ||||
-rw-r--r-- | tests/type/json/buildfile | 4 | ||||
-rw-r--r-- | tests/type/json/testscript | 416 |
16 files changed, 3506 insertions, 24 deletions
diff --git a/doc/buildfile b/doc/buildfile index 633505b..5508ddb 100644 --- a/doc/buildfile +++ b/doc/buildfile @@ -76,6 +76,7 @@ xhtml{*}: extension = xhtml for ff: functions-builtin \ functions-string \ functions-integer \ + functions-json \ functions-bool \ functions-path \ functions-name \ diff --git a/doc/manual.cli b/doc/manual.cli index 66d0ffb..cd77bfe 100644 --- a/doc/manual.cli +++ b/doc/manual.cli @@ -6113,6 +6113,29 @@ if $regex.match($name, '(.+)-(.+)') " source <functions-regex.cli>; +// $json.*() +// +" +\h#functions-json|JSON Functions| + +The \c{$json.*()} function family contains function that operate on the JSON +types: \c{json}, \c{json_array}, and \c{json_object}. For example: + +\ +j = [json] one@1 two@abc three@([json] x@1 y@-1) + +for m: $j +{ + n = $member_name($m) + v = $member_value($m) + + info $n $value_type($v) $v +} +\ + +" +source <functions-json.cli>; + // $process.*() // diff --git a/libbuild2/function.cxx b/libbuild2/function.cxx index f979d92..3110547 100644 --- a/libbuild2/function.cxx +++ b/libbuild2/function.cxx @@ -355,6 +355,7 @@ namespace build2 void builtin_functions (function_map&); // functions-builtin.cxx void filesystem_functions (function_map&); // functions-filesystem.cxx void integer_functions (function_map&); // functions-integer.cxx + void json_functions (function_map&); // functions-json.cxx void name_functions (function_map&); // functions-name.cxx void path_functions (function_map&); // functions-path.cxx void process_functions (function_map&); // functions-process.cxx @@ -365,6 +366,7 @@ namespace build2 void target_triplet_functions (function_map&); // functions-target-triplet.cxx void project_name_functions (function_map&); // functions-target-triplet.cxx + void insert_builtin_functions (function_map& m) { @@ -372,6 +374,7 @@ namespace build2 builtin_functions (m); filesystem_functions (m); integer_functions (m); + json_functions (m); name_functions (m); path_functions (m); process_functions (m); diff --git a/libbuild2/functions-json.cxx b/libbuild2/functions-json.cxx new file mode 100644 index 0000000..7551fa1 --- /dev/null +++ b/libbuild2/functions-json.cxx @@ -0,0 +1,240 @@ +// file : libbuild2/functions-json.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/function.hxx> +#include <libbuild2/variable.hxx> + +#ifndef BUILD2_BOOTSTRAP +# include <libbutl/json/parser.hxx> +# include <libbutl/json/serializer.hxx> +#endif + +using namespace std; + +namespace build2 +{ + static size_t + find_index (const json_value& a, value v) + { + if (a.type != json_type::array) + fail << "expected json array instead of " << to_string (a.type) + << " as first argument"; + + auto b (a.array.begin ()), e (a.array.end ()); + auto i (find (b, e, convert<json_value> (move (v)))); + return i != e ? i - b : a.array.size (); + }; + + void + json_functions (function_map& m) + { + function_family f (m, "json"); + + // $value_type(<json>[, <distinguish_numbers>]) + // + // Return the type of a JSON value: `null`, `boolean`, `number`, `string`, + // `array`, or `object`. If the <distinguish_numbers> argument is `true`, + // then instead of `number` return `signed number`, `unsigned number`, or + // `hexadecimal number`. + // + f["value_type"] += [] (json_value v, optional<value> distinguish_numbers) + { + bool dn (distinguish_numbers && + convert<bool> (move (*distinguish_numbers))); + + return to_string (v.type, dn); + }; + + // $member_name(<json>) + // + // Return the name of a JSON object member. + // + f["member_name"] += [] (json_value v) + { + // A member becomes an object with a single member (see json_reverse() + // for details). + // + if (v.type == json_type::object && v.object.size () == 1) + return move (v.object.front ().name); + + fail << "json object member expected instead of " << v.type << endf; + }; + + // $member_value(<json>) + // + // Return the value of a JSON object member. + // + f["member_value"] += [] (json_value v) + { + // A member becomes an object with a single member (see json_reverse() + // for details). + // + if (v.type == json_type::object && v.object.size () == 1) + return move (v.object.front ().value); + + fail << "json object member expected instead of " << v.type << endf; + }; + + // $size(<json>) + // + // Return the size of a JSON value. + // + // The size of a `null` value is `0`. The sizes of simple values + // (`boolean`, `number`, and `string`) is `1`. The size of `array` and + // `object` values is the number of elements and members, respectively. + // + // Note that the size of a `string` JSON value is not the length of the + // string. To get the length call `$string.size()` instead by casting the + // JSON value to the `string` value type. + // + f["size"] += [] (json_value v) -> size_t + { + // Note: should be consistent with value_traits<json_value>::empty(), + // json_subscript(). + // + switch (v.type) + { + case json_type::null: return 0; + case json_type::boolean: + case json_type::signed_number: + case json_type::unsigned_number: + case json_type::hexadecimal_number: + case json_type::string: break; + case json_type::array: return v.array.size (); + case json_type::object: return v.object.size (); + } + + return 1; + }; + + // $find(<json-array>, <json>) + // + // Return true if the JSON array contains the specified JSON value. + // + f["find"] += [](json_value a, value v) + { + size_t i (find_index (a, move (v))); + return i != a.array.size (); // We now know it's an array. + }; + + // $find_index(<json-array>, <json>) + // + // Return the index of the first element in the JSON array that is equal + // to the specified JSON value or `$size(json-array)` if none is found. + // + f["find_index"] += [](json_value a, value v) + { + return find_index (a, move (v)); + }; + +#ifndef BUILD2_BOOTSTRAP + + // @@ Flag to support multi-value (returning it as JSON array)? Then + // probably also in $serialize(). + // + // @@ Flag to override duplicates instead of failing? + + // $json.load(<path>) + // + // Parse the contents of the specified file as JSON input text and return + // the result as a value of the `json` type. + // + // See also `$json.parse()`. + // + // Note that this function is not pure. + // + f.insert (".load", false) += [] (names xf) + { + path f (convert<path> (move (xf))); + + try + { + ifdstream is (f); + json_parser p (is, f.string ()); + return json_value (p); + } + catch (const invalid_json_input& e) + { + fail (location (f, e.line, e.column)) << "invalid json input: " << e << + info << "byte offset " << e.position << endf; + } + catch (const io_error& e) + { + fail << "unable to read from " << f << ": " << e << endf; + } + }; + + // $json.parse(<text>) + // + // Parse the specified JSON input text and return the result as a value of + // the `json` type. + // + // See also `$json.load()` and `$json.serialize()`. + // + f[".parse"] += [] (names text) + { + string t (convert<string> (move (text))); + + try + { + json_parser p (t, nullptr /* name */); + return json_value (p); + } + catch (const invalid_json_input& e) + { + fail << "invalid json input: " << e << + info << "line " << e.line + << ", column " << e.column + << ", byte offset " << e.position << endf; + } + }; + + // $serialize(<json>[, <indentation>]) + // + // Serialize the specified JSON value and return the resulting JSON output + // text. + // + // The optional <indentation> argument specifies the number of indentation + // spaces that should be used for pretty-printing. If `0` is passed, then + // no pretty-printing is performed. The default is `2` spaces. + // + // See also `$json.parse()`. + // + f["serialize"] += [] (json_value v, optional<value> indentation) + { + uint64_t i (indentation ? convert<uint64_t> (*indentation) : 2); + + try + { + // For the diagnostics test. + // +#if 0 + if (v.type == json_type::string && v.string == "deadbeef") + { + v.string[4] = 0xe0; + v.string[5] = 0xe0; + } +#endif + + string o; + json_buffer_serializer s (o, i); + v.serialize (s); + return o; + } + catch (const invalid_json_output& e) + { + diag_record dr; + dr << fail << "invalid json value: " << e; + + if (e.event) + dr << info << "while serializing " << to_string (*e.event); + + if (e.offset != string::npos) + dr << info << "offending byte offset " << e.offset; + + dr << endf; + } + }; +#endif + } +} diff --git a/libbuild2/json.cxx b/libbuild2/json.cxx new file mode 100644 index 0000000..f368679 --- /dev/null +++ b/libbuild2/json.cxx @@ -0,0 +1,856 @@ +// file : libbuild2/json.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include <libbuild2/json.hxx> + +#include <limits> + +#ifndef BUILD2_BOOTSTRAP +# include <libbutl/json/parser.hxx> +# include <libbutl/json/serializer.hxx> +#endif + +namespace build2 +{ + // json_event + // +#ifndef BUILD2_BOOTSTRAP + const char* + to_string (json_event e) + { + switch (e) + { + case json_event::begin_object: return "beginning of object"; + case json_event::end_object: return "end of object"; + case json_event::begin_array: return "beginning of array"; + case json_event::end_array: return "end of array"; + case json_event::name: return "member name"; + case json_event::string: return "string value"; + case json_event::number: return "numeric value"; + case json_event::boolean: return "boolean value"; + case json_event::null: return "null value"; + } + + return ""; + } +#endif + + // json_type + // + const char* + to_string (json_type t, bool dn) noexcept + { + using type = json_type; + + switch (t) + { + case type::null: return "null"; + case type::boolean: return "boolean"; + case type::signed_number: return dn ? "signed number" : "number"; + case type::unsigned_number: return dn ? "unsigned number" : "number"; + case type::hexadecimal_number: return dn ? "hexadecimal number" : "number"; + case type::string: return "string"; + case type::array: return "array"; + case type::object: return "object"; + } + return ""; + } + + // json_value + // + const json_value null_json_value (json_type::null); + + [[noreturn]] void + at_throw (json_type t, json_type e, bool index) + { + string m; + + if (t != e && t != json_type::null) + { + m = "expected "; + m += to_string (e); + m += " instead of "; + m += to_string (t); + throw invalid_argument (move (m)); + } + else + { + m = index ? "index" : "name"; + m += " out of range in "; + m += to_string (e); + throw std::out_of_range (move (m)); + } + } + + const json_value& json_value:: + at (size_t index) const + { + if (type == json_type::array) + { + if (index < array.size ()) + return array[index]; + } + + at_throw (type, json_type::array, true); + } + + json_value& json_value:: + at (size_t index) + { + if (type == json_type::array) + { + if (index < array.size ()) + return array[index]; + } + + at_throw (type, json_type::array, true); + } + + const json_value& json_value:: + operator[] (size_t index) const + { + if (type == json_type::null) + return null_json_value; + + if (type == json_type::array) + return index < array.size () ? array[index] : null_json_value; + + at_throw (type, json_type::array, true); + } + + json_value& json_value:: + operator[] (size_t index) + { + if (type == json_type::null) + { + new (&array) array_type (); + type = json_type::array; + } + + if (type == json_type::array) + { + size_t n (array.size ()); + + if (index < n) + return array[index]; + + // If there are missing elements in between, fill them with nulls. + // + if (index != n) + array.resize (index, json_value ()); + + array.push_back (json_value ()); + return array.back (); + } + + at_throw (type, json_type::array, true); + } + + const json_value& json_value:: + at (const char* name) const + { + if (type == json_type::object) + { + auto i (find_if (object.begin (), object.end (), + [name] (const json_member& m) + { + return m.name == name; + })); + + if (i != object.end ()) + return i->value; + } + + at_throw (type, json_type::object, false); + } + + json_value& json_value:: + at (const char* name) + { + if (type == json_type::object) + { + auto i (find_if (object.begin (), object.end (), + [name] (const json_member& m) + { + return m.name == name; + })); + + if (i != object.end ()) + return i->value; + } + + at_throw (type, json_type::object, false); + } + + const json_value& json_value:: + operator[] (const char* name) const + { + if (type == json_type::null) + return null_json_value; + + if (type == json_type::object) + { + auto i (find_if (object.begin (), object.end (), + [name] (const json_member& m) + { + return m.name == name; + })); + + + return i != object.end () ? i->value : null_json_value; + } + + at_throw (type, json_type::object, false); + } + + json_value& json_value:: + operator[] (const char* name) + { + if (type == json_type::null) + { + new (&object) object_type (); + type = json_type::object; + } + + if (type == json_type::object) + { + auto i (find_if (object.begin (), object.end (), + [name] (const json_member& m) + { + return m.name == name; + })); + + if (i != object.end ()) + return i->value; + + object.push_back (json_member {name, json_value ()}); + return object.back ().value; + } + + at_throw (type, json_type::object, false); + } + + int json_value:: + compare (const json_value& v) const noexcept + { + int r (0); + { + // Note: we need to treat unsigned and hexadecimal the same. + // + json_type t (type == json_type::hexadecimal_number + ? json_type::unsigned_number + : type); + + json_type vt (v.type == json_type::hexadecimal_number + ? json_type::unsigned_number + : v.type); + + if (t != vt) + { + // Handle the special signed/unsigned number case here. + // + if (t == json_type::signed_number && + vt == json_type::unsigned_number) + { + if (signed_number < 0) + r = -1; + else + { + uint64_t u (static_cast<uint64_t> (signed_number)); + r = u < v.unsigned_number ? -1 : (u > v.unsigned_number ? 1 : 0); + } + } + else if (t == json_type::unsigned_number && + vt == json_type::signed_number) + { + if (v.signed_number < 0) + r = 1; + else + { + uint64_t u (static_cast<uint64_t> (v.signed_number)); + r = unsigned_number < u ? -1 : (unsigned_number > u ? 1 : 0); + } + } + else + r = (static_cast<uint8_t> (t) < static_cast<uint8_t> (vt) ? -1 : 1); + } + } + + if (r == 0) + { + switch (type) + { + case json_type::null: + { + r = 0; + break; + } + case json_type::boolean: + { + r = boolean == v.boolean ? 0 : boolean ? 1 : -1; + break; + } + case json_type::signed_number: + { + r = (signed_number < v.signed_number + ? -1 + : (signed_number > v.signed_number ? 1 : 0)); + break; + } + case json_type::unsigned_number: + case json_type::hexadecimal_number: + { + r = (unsigned_number < v.unsigned_number + ? -1 + : (unsigned_number > v.unsigned_number ? 1 : 0)); + break; + } + case json_type::string: + { + r = string.compare (v.string); + break; + } + case json_type::array: + { + auto i (array.begin ()), ie (array.end ()); + auto j (v.array.begin ()), je (v.array.end ()); + + for (; i != ie && j != je; ++i, ++j) + { + if ((r = i->compare (*j)) != 0) + break; + } + + if (r == 0) + r = i == ie ? (j == je ? 0 : -1) : 1; // More elements than other? + + break; + } + case json_type::object: + { + // We don't expect there to be a large number of members so it makes + // sense to iterate in the lexicographical order without making any + // copies. + // + auto next = [] (object_type::const_iterator p, // == e for first + object_type::const_iterator b, + object_type::const_iterator e) + { + // We need to find an element with the "smallest" name that is + // greater than the previous entry. + // + auto n (e); + + for (auto i (b); i != e; ++i) + { + if (p == e || i->name > p->name) + { + int r; + if (n == e || (r = n->name.compare (i->name)) > 0) + n = i; + else + assert (r != 0); // No duplicates. + } + } + + return n; + }; + + auto ib (object.begin ()), ie (object.end ()), i (ie); + auto jb (v.object.begin ()), je (v.object.end ()), j (je); + + for (;;) + { + // Note: we must call next() on both. + // + i = next (i, ib, ie); + j = next (j, jb, je); + + if (i == ie || j == je) + break; + + // Determine if both have this name and if not, which name comes + // first. + // + int n (i->name.compare (j->name)); + + r = (n < 0 // If i's first, then i is greater. + ? -1 + : (n > 0 // If j's first, then j is greater. + ? 1 + : i->value.compare (j->value))); // Both have this name. + + if (r != 0) + break; + } + + if (r == 0) + r = i == ie ? (j == je ? 0 : -1) : 1; // More members than other? + + break; + } + } + } + + return r; + } + + static void + append_numbers (json_value& l, const json_value& r) noexcept + { + auto append = [&l] (uint64_t u, int64_t s, bool hex = false) + { + if (s < 0) + { + // The absolute value of a minimum signed intereger is not + // representable in the 2s complement integers. So handle this + // specially for completeness. + // + uint64_t a ( + s != std::numeric_limits<int64_t>::min () + ? static_cast<uint64_t> (-s) + : static_cast<uint64_t> (std::numeric_limits<int64_t>::max ()) + 1); + + if (u >= a) + { + l.unsigned_number = u - a; + l.type = (hex + ? json_type::hexadecimal_number + : json_type::unsigned_number); + } + else + { + l.signed_number = -static_cast<int64_t> (a - u); + l.type = json_type::signed_number; + } + } + else + { + l.unsigned_number = u + static_cast<uint64_t> (s); + l.type = (hex + ? json_type::hexadecimal_number + : json_type::unsigned_number); + } + }; + + // We try to keep LHS hex if possible. + // + if (l.type == json_type::signed_number) + { + if (r.type == json_type::signed_number) + { + // Deal with non-negative signed numbers for completeness. + // + if (l.signed_number >= 0) + append (static_cast <uint64_t> (l.signed_number), r.signed_number); + else if (r.signed_number >= 0) + append (static_cast <uint64_t> (r.signed_number), l.signed_number); + else + l.signed_number += r.signed_number; + } + else + append (r.unsigned_number, l.signed_number); + } + else + { + if (r.type == json_type::signed_number) + append (l.unsigned_number, + r.signed_number, + l.type == json_type::hexadecimal_number); + else + l.unsigned_number += r.unsigned_number; + } + } + + void json_value:: + append (json_value&& v) + { + if (type == json_type::null) + { + *this = move (v); + return; + } + else if (type == json_type::array) + { + if (v.type == json_type::array) + { + if (array.empty ()) + array = move (v.array); + else + array.insert (array.end (), + make_move_iterator (v.array.begin ()), + make_move_iterator (v.array.end ())); + } + else + array.push_back (move (v)); + + return; + } + else + { + switch (v.type) + { + case json_type::null: return; + case json_type::boolean: + { + if (type != json_type::boolean) + break; + + boolean = boolean || v.boolean; + return; + } + case json_type::signed_number: + case json_type::unsigned_number: + case json_type::hexadecimal_number: + { + if (type != json_type::signed_number && + type != json_type::unsigned_number && + type != json_type::hexadecimal_number) + break; + + append_numbers (*this, v); + return; + } + case json_type::string: + { + if (type != json_type::string) + break; + + string += v.string; + return; + } + case json_type::array: break; + case json_type::object: + { + if (type != json_type::object) + break; + + if (object.empty ()) + object = move (v.object); + else + { + for (json_member& m: v.object) + { + auto i (find_if (object.begin (), object.end (), + [&m] (const json_member& o) + { + return m.name == o.name; + })); + if (i != object.end ()) + i->value = move (m.value); + else + object.push_back (move (m)); + } + } + + return; + } + } + } + + throw invalid_argument ( + string_type ("unable to append ") + to_string (v.type) + " to " + + to_string (type)); + } + + void json_value:: + prepend (json_value&& v) + { + if (type == json_type::null) + { + *this = move (v); + return; + } + else if (type == json_type::array) + { + if (v.type == json_type::array) + { + if (array.empty ()) + array = move (v.array); + else + array.insert (array.begin (), + make_move_iterator (v.array.begin ()), + make_move_iterator (v.array.end ())); + } + else + array.insert (array.begin (), move (v)); + + return; + } + else + { + switch (v.type) + { + case json_type::null: return; + case json_type::boolean: + { + if (type != json_type::boolean) + break; + + boolean = boolean || v.boolean; + return; + } + case json_type::signed_number: + case json_type::unsigned_number: + case json_type::hexadecimal_number: + { + if (type != json_type::signed_number && + type != json_type::unsigned_number && + type != json_type::hexadecimal_number) + break; + + append_numbers (*this, v); + return; + } + case json_type::string: + { + if (type != json_type::string) + break; + + string.insert (0, v.string); + return; + } + case json_type::array: break; + case json_type::object: + { + if (type != json_type::object) + break; + + if (object.empty ()) + object = move (v.object); + else + { + for (json_member& m: v.object) + { + auto i (find_if (object.begin (), object.end (), + [&m] (const json_member& o) + { + return m.name == o.name; + })); + if (i != object.end ()) + i->value = move (m.value); + else + object.insert (object.begin (), move (m)); + } + } + + return; + } + } + } + + throw invalid_argument ( + string_type ("unable to prepend ") + to_string (v.type) + " to " + + to_string (type)); + } + +#ifndef BUILD2_BOOTSTRAP + json_value:: + json_value (json_parser& p, optional<json_type> et) + { + using namespace butl::json; + + // A JSON input text cannot be empty. + // + // Once we have JSON5 support we will be able to distinguish hexadecimal + // numbers. + // + json_type t (json_type::null); + switch (*p.next ()) + { + case event::begin_object: t = json_type::object; break; + case event::begin_array: t = json_type::array; break; + case event::string: t = json_type::string; break; + case event::number: t = (p.value ()[0] == '-' + ? json_type::signed_number + : json_type::unsigned_number); break; + case event::boolean: t = json_type::boolean; break; + case event::null: t = json_type::null; break; + case event::name: + case event::end_array: + case event::end_object: + { + assert (false); + type = json_type::null; + return; + } + } + + if (et && *et != t) + { + throw invalid_json_input ( + p.input_name != nullptr ? p.input_name : "", + p.line (), + p.column (), + p.position (), + string_type ("expected ") + to_string (*et) + " instead of " + + to_string (t)); + } + + switch (t) + { + case json_type::object: + { + object_type o; // For exception safety. + while (*p.next () != event::end_object) + { + string_type n (p.name ()); + + // Check for duplicates. For now we fail but in the future we may + // provide a mode (via a flag) to override instead. + // + if (find_if (o.begin (), o.end (), + [&n] (const json_member& m) + { + return m.name == n; + }) != o.end ()) + { + throw invalid_json_input ( + p.input_name != nullptr ? p.input_name : "", + p.line (), + p.column (), + p.position (), + "duplicate object member '" + n + '\''); + } + + o.push_back (json_member {move (n), json_value (p)}); + } + + new (&object) object_type (move (o)); + type = t; + break; + } + case json_type::array: + { + array_type c; // For exception safety. + while (*p.peek () != event::end_array) + c.push_back (json_value (p)); + p.next (); // Consume end_array. + + new (&array) array_type (move (c)); + type = t; + break; + } + case json_type::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 = t; + break; + } + case json_type::signed_number: + { + signed_number = p.value<int64_t> (); + type = t; + break; + } + case json_type::unsigned_number: + case json_type::hexadecimal_number: + { + unsigned_number = p.value<uint64_t> (); + type = t; + break; + } + case json_type::boolean: + { + boolean = p.value<bool> (); + type = t; + break; + } + case json_type::null: + { + type = t; + break; + } + } + } + + void json_value:: + serialize (json_buffer_serializer& s, optional<json_type> et) const + { + using namespace butl::json; + + if (et && *et != type) + { + throw invalid_json_output ( + nullopt, + invalid_json_output::error_code::invalid_value, + string_type ("expected ") + to_string (*et) + " instead of " + + to_string (type)); + } + + switch (type) + { + case json_type::null: + { + s.value (nullptr); + break; + } + case json_type::boolean: + { + s.value (boolean); + break; + } + case json_type::signed_number: + { + s.value (signed_number); + break; + } + case json_type::unsigned_number: + case json_type::hexadecimal_number: + { + // When we have JSON5 support, we will be able to serialize + // hexadecimal properly. + // + s.value (unsigned_number); + break; + } + case json_type::string: + { + s.value (string); + break; + } + case json_type::array: + { + s.begin_array (); + for (const json_value& e: array) + e.serialize (s); + s.end_array (); + break; + } + case json_type::object: + { + s.begin_object (); + for (const json_member& m: object) + { + s.member_name (m.name); + m.value.serialize (s); + } + s.end_object (); + break; + } + } + } + +#else + json_value:: + json_value (json_parser&, optional<json_type>) + { + assert (false); + type = json_type::null; + } + + void json_value:: + serialize (json_buffer_serializer&, optional<json_type>) const + { + assert (false); + } +#endif +} diff --git a/libbuild2/json.hxx b/libbuild2/json.hxx new file mode 100644 index 0000000..1f2694d --- /dev/null +++ b/libbuild2/json.hxx @@ -0,0 +1,316 @@ +// 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 + { + enum class event: uint8_t; + class parser; + class buffer_serializer; + class stream_serializer; + class invalid_json_input; + class invalid_json_output; + } +} + +namespace build2 +{ + using json_event = butl::json::event; + using json_parser = butl::json::parser; + using json_buffer_serializer = butl::json::buffer_serializer; + using json_stream_serializer = butl::json::stream_serializer; + using butl::json::invalid_json_input; + using butl::json::invalid_json_output; + +#ifndef BUILD2_BOOTSTRAP + LIBBUILD2_SYMEXPORT const char* + to_string (json_event); +#endif + + // @@ TODO: + // + // - provide swap(). + // - provide operator=(uint64_t), etc. + // - provide std::hash specialization + // - tighted at()/[] interface in json_array and json_object + // - tighten noexcep where possible + // - operator bool() - in a sense null is like nullopt. + // + + // This JSON representation has one extensions compared to the standard JSON + // model: it distinguishes between signed, unsigned, and hexadecimal + // numbers. + // + // Note also that we don't assume that object members are in a sorted order + // (but do assume there are no duplicates). However, we could add an + // argument to signal that this is the case to speed up some functions, for + // example, compare(). + // + enum class json_type: uint8_t + { + null, // Note: keep first for comparison. + boolean, + signed_number, + unsigned_number, + hexadecimal_number, + string, + array, + object, + }; + + // Return the JSON type as string. If distinguish_numbers is true, then + // distinguish between the singned, unsigned, and hexadecimal types. + // + LIBBUILD2_SYMEXPORT const char* + to_string (json_type, bool distinguish_numbers = false) noexcept; + + inline ostream& + operator<< (ostream& os, json_type t) {return os << to_string (t);} + + struct json_member; + + class LIBBUILD2_SYMEXPORT json_value + { + public: + using string_type = build2::string; + using array_type = vector<json_value>; + using object_type = vector<json_member>; + + json_type type; + + union + { + bool boolean; + int64_t signed_number; + uint64_t unsigned_number; // Also used for hexadecimal_number. + string_type string; + array_type array; + object_type object; + }; + + explicit + json_value (json_type = json_type::null) noexcept; + + explicit + json_value (std::nullptr_t) noexcept; + + explicit + json_value (bool) noexcept; + + explicit + json_value (int64_t) noexcept; + + explicit + json_value (uint64_t, bool hexadecimal = false) noexcept; + + explicit + json_value (string_type); + + // If the expected type is specfied, then fail if it does not match + // parsed. Throws invalid_json_input. + // + explicit + json_value (json_parser&, optional<json_type> expected = {}); + + // If the expected type is specfied, then fail if it does not match the + // value's. Throws invalid_json_output. + // + void + serialize (json_buffer_serializer&, + optional<json_type> expected = {}) const; + + // Note that values of different types are never equal, except for + // signed/unsigned/hexadecimal numbers. Null is equal to null and is less + // than any other value. Arrays are compared lexicographically. Object + // members are considered in the lexicographically-compared name-ascending + // order (see RFC8785). An absent member is less than a present member + // (even if it's null). + // + int + compare (const json_value&) const noexcept; + + // Append/prepend one JSON value to another. Throw invalid_argument if the + // values are incompatible. Note that for numbers this can also lead to + // the change of the value type. + // + // Append/prepend to an object overrides existing members. Append/prepend + // an array to an array splices in the array elements rather than adding + // an element of the array type. + // + void + append (json_value&&); + + void + prepend (json_value&&); + + + // Array element access. + // + // If the index is out of array bounds, the at() functions throw + // std::out_of_range, the const operator[] returns null_json_value, and + // the non-const operator[] inserts a new null value at the specified + // position (filling any missing elements in between with nulls) and + // returns that. All three functions throw std::invalid_argument if the + // value is not an array or null with null treated as (missing) array + // rather than wrong value type (and with at() functons throwing + // out_of_range in this case). + // + // Note that non-const operator[] will not only insert a new element but + // will also turn the value it is called upon into array if it is null. + // This semantics allows you to string several subscripts to build up a + // chain of values. + // + // Note also that while the operator[] interface is convenient for + // accessing and modifying (or building up) values deep in the tree, it + // can lead to inefficiencies or even undesirable semantics during + // otherwise read-only access of a non-const object due to the potential + // insertion of null values for missing array elements. As a result, it's + // recommended to alwas use a const reference for read-only access (or use + // the at() interface if this is deemed too easy to forget). + // + const json_value& + at (size_t) const; + + json_value& + at (size_t); + + const json_value& + operator[] (size_t) const; + + json_value& + operator[] (size_t); + + + // Object member access. + // + // If a member with the specified name is not found in the object, the + // at() functions throw std::out_of_range, the const operator[] returns + // null_json_value, and the non-const operator[] adds a new member with + // the specified name and null value and returns that value. All three + // functions throw std::invalid_argument if the value is not an array or + // null with null treated as (missing) object rather than wrong value type + // (and with at() functons throwing out_of_range in this case). + // + // Note that non-const operator[] will not only insert a new member but + // will also turn the value it is called upon into object if it is null. + // This semantics allows you to string several subscripts to build up a + // chain of values. + // + // Note also that while the operator[] interface is convenient for + // accessing and modifying (or building up) values deep in the tree, it + // can lead to inefficiencies or even undesirable semantics during + // otherwise read-only access of a non-const object due to the potential + // insertion of null values for missing object members. As a result, it's + // recommended to alwas use a const reference for read-only access (or use + // the at() interface if this is deemed too easy to forget). + // + const json_value& + at (const char*) const; + + json_value& + at (const char*); + + const json_value& + operator[] (const char*) const; + + json_value& + operator[] (const char*); + + const json_value& + at (const string_type&) const; + + json_value& + at (const string_type&); + + const json_value& + operator[] (const string_type&) const; + + json_value& + operator[] (const string_type&); + + // Note that the moved-from value becomes JSON null value. + // + json_value (json_value&&) noexcept; + json_value (const json_value&); + + json_value& operator= (json_value&&) noexcept; + json_value& operator= (const json_value&); + + ~json_value () noexcept; + }; + + LIBBUILD2_SYMEXPORT extern const json_value null_json_value; + + inline bool + operator== (const json_value& x, const json_value& y) {return x.compare (y) == 0;} + + inline bool + operator!= (const json_value& x, const json_value& y) {return !(x == y);} + + inline bool + operator< (const json_value& x, const json_value& y) {return x.compare (y) < 0;} + + inline bool + operator<= (const json_value& x, const json_value& y) {return x.compare (y) <= 0;} + + inline bool + operator> (const json_value& x, const json_value& y) {return !(x <= y);} + + inline bool + operator>= (const json_value& x, const json_value& y) {return !(x < y);} + + // A JSON object member. + // + struct json_member + { + // @@ TODO: add some convenience constructors? + + string name; + json_value value; + }; + + // A JSON value that can only be an array. + // + class /*LIBBUILD2_SYMEXPORT*/ json_array: public json_value + { + public: + // Create empty array. + // + json_array () noexcept; + + explicit + json_array (json_parser&); + + void + serialize (json_buffer_serializer& s) const; + }; + + // A JSON value that can only be an object. + // + class /*LIBBUILD2_SYMEXPORT*/ json_object: public json_value + { + public: + // Create empty object. + // + json_object () noexcept; + + explicit + json_object (json_parser&); + + void + serialize (json_buffer_serializer& s) const; + }; +} + +#include <libbuild2/json.ixx> + +#endif // LIBBUILD2_JSON_HXX diff --git a/libbuild2/json.ixx b/libbuild2/json.ixx new file mode 100644 index 0000000..c2b8845 --- /dev/null +++ b/libbuild2/json.ixx @@ -0,0 +1,222 @@ +// file : libbuild2/json.ixx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +namespace build2 +{ + inline json_value:: + ~json_value () noexcept + { + switch (type) + { + case json_type::null: + case json_type::boolean: + case json_type::signed_number: + case json_type::unsigned_number: + case json_type::hexadecimal_number: break; + case json_type::string: string.~string_type (); break; + case json_type::array: array.~array_type (); break; + case json_type::object: object.~object_type (); break; + } + } + + inline json_value:: + json_value (json_type t) noexcept + : 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: + case json_type::hexadecimal_number: unsigned_number = 0; break; + case json_type::string: new (&string) string_type (); break; + case json_type::array: new (&array) array_type (); break; + case json_type::object: new (&object) object_type (); break; + } + } + + inline json_value:: + json_value (std::nullptr_t) noexcept + : type (json_type::null) + { + } + + inline json_value:: + json_value (bool v) noexcept + : type (json_type::boolean), boolean (v) + { + } + + inline json_value:: + json_value (int64_t v) noexcept + : type (json_type::signed_number), signed_number (v) + { + } + + inline json_value:: + json_value (uint64_t v, bool hex) noexcept + : type (hex + ? json_type::hexadecimal_number + : json_type::unsigned_number), + unsigned_number (v) + { + } + + inline json_value:: + json_value (string_type v) + : type (json_type::string), string (move (v)) + { + } + + inline const json_value& json_value:: + at (const string_type& n) const + { + return at (n.c_str ()); + } + + inline json_value& json_value:: + at (const string_type& n) + { + return at (n.c_str ()); + } + + inline const json_value& json_value:: + operator[] (const string_type& n) const + { + return operator[] (n.c_str ()); + } + + inline json_value& json_value:: + operator[] (const string_type& n) + { + return operator[] (n.c_str ()); + } + + inline json_value:: + json_value (json_value&& v) noexcept + : type (v.type) + { + 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: + case json_type::hexadecimal_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: + new (&array) array_type (move (v.array)); + v.array.~array_type (); + break; + case json_type::object: + new (&object) object_type (move (v.object)); + v.object.~object_type (); + break; + } + + v.type = json_type::null; + } + + inline json_value:: + json_value (const json_value& v) + : type (v.type) + { + 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: + case json_type::hexadecimal_number: + unsigned_number = v.unsigned_number; + break; + case json_type::string: + new (&string) string_type (v.string); + break; + case json_type::array: + new (&array) array_type (v.array); + break; + case json_type::object: + new (&object) object_type (v.object); + break; + } + } + + inline json_value& json_value:: + operator= (json_value&& v) noexcept + { + if (this != &v) + { + this->~json_value (); + new (this) json_value (move (v)); + } + return *this; + } + + inline json_value& json_value:: + operator= (const json_value& v) + { + if (this != &v) + { + this->~json_value (); + new (this) json_value (v); + } + return *this; + } + + // json_array + // + inline json_array:: + json_array () noexcept + : json_value (json_type::array) + { + } + + inline json_array:: + json_array (json_parser& p) + : json_value (p, json_type::array) + { + } + + inline void json_array:: + serialize (json_buffer_serializer& s) const + { + json_value::serialize (s, json_type::array); + } + + // json_object + // + inline json_object:: + json_object () noexcept + : json_value (json_type::object) + { + } + + inline json_object:: + json_object (json_parser& p) + : json_value (p, json_type::object) + { + } + + inline void json_object:: + serialize (json_buffer_serializer& s) const + { + json_value::serialize (s, json_type::object); + } +} diff --git a/libbuild2/parser.cxx b/libbuild2/parser.cxx index 043cd10..1ac159b 100644 --- a/libbuild2/parser.cxx +++ b/libbuild2/parser.cxx @@ -5761,30 +5761,106 @@ namespace build2 const value_type* parser:: find_value_type (const scope*, const string& n) { - auto ptr = [] (const value_type& vt) {return &vt;}; - - return - n == "bool" ? ptr (value_traits<bool>::value_type) : - n == "int64" ? ptr (value_traits<int64_t>::value_type) : - n == "uint64" ? ptr (value_traits<uint64_t>::value_type) : - n == "string" ? ptr (value_traits<string>::value_type) : - n == "path" ? ptr (value_traits<path>::value_type) : - n == "dir_path" ? ptr (value_traits<dir_path>::value_type) : - n == "abs_dir_path" ? ptr (value_traits<abs_dir_path>::value_type) : - n == "name" ? ptr (value_traits<name>::value_type) : - n == "name_pair" ? ptr (value_traits<name_pair>::value_type) : - n == "target_triplet" ? ptr (value_traits<target_triplet>::value_type) : - n == "project_name" ? ptr (value_traits<project_name>::value_type) : - - n == "int64s" ? ptr (value_traits<int64s>::value_type) : - n == "uint64s" ? ptr (value_traits<uint64s>::value_type) : - n == "strings" ? ptr (value_traits<strings>::value_type) : - n == "paths" ? ptr (value_traits<paths>::value_type) : - n == "dir_paths" ? ptr (value_traits<dir_paths>::value_type) : - n == "names" ? ptr (value_traits<vector<name>>::value_type) : - n == "cmdline" ? ptr (value_traits<cmdline>::value_type) : - - nullptr; + switch (n[0]) + { + case 'a': + { + if (n == "abs_dir_path") return &value_traits<abs_dir_path>::value_type; + break; + } + case 'b': + { + if (n == "bool") return &value_traits<bool>::value_type; + break; + } + case 'c': + { + if (n == "cmdline") return &value_traits<cmdline>::value_type; + break; + } + case 'd': + { + if (n.compare (0, 8, "dir_path") == 0) + { + if (n[8] == '\0') return &value_traits<dir_path>::value_type; + if (n[8] == 's' && + n[9] == '\0') return &value_traits<dir_paths>::value_type; + } + break; + } + case 'i': + { + if (n.compare (0, 5, "int64") == 0) + { + if (n[5] == '\0') return &value_traits<int64_t>::value_type; + if (n[5] == 's' && + n[6] == '\0') return &value_traits<int64s>::value_type; + } + break; + } + case 'j': + { + if (n.compare (0, 4, "json") == 0) + { + if (n[4] == '\0') return &value_traits<json_value>::value_type; + if (n == "json_array") return &value_traits<json_array>::value_type; + if (n == "json_object") return &value_traits<json_object>::value_type; + } + break; + } + case 'n': + { + if (n.compare (0, 4, "name") == 0) + { + if (n[4] == '\0') return &value_traits<name>::value_type; + if (n[4] == 's' && + n[5] == '\0') return &value_traits<vector<name>>::value_type; + if (n == "name_pair") return &value_traits<name_pair>::value_type; + } + break; + } + + case 'p': + { + if (n.compare (0, 4, "path") == 0) + { + if (n[4] == '\0') return &value_traits<path>::value_type; + if (n[4] == 's' && + n[5] == '\0') return &value_traits<paths>::value_type; + } + else if (n == "project_name") return &value_traits<project_name>::value_type; + break; + } + case 's': + { + if (n.compare (0, 6, "string") == 0) + { + if (n[6] == '\0') return &value_traits<string>::value_type; + if (n[6] == 's' && + n[7] == '\0') return &value_traits<strings>::value_type; + } + break; + } + case 't': + { + if (n == "target_triplet") return &value_traits<target_triplet>::value_type; + break; + } + case 'u': + { + if (n.compare (0, 6, "uint64") == 0) + { + if (n[6] == '\0') return &value_traits<uint64_t>::value_type; + if (n[6] == 's' && + n[7] == '\0') return &value_traits<uint64s>::value_type; + } + break; + } + default: + break; + } + + return nullptr; } void parser:: diff --git a/libbuild2/variable.cxx b/libbuild2/variable.cxx index 0ad63b1..b0fe32a 100644 --- a/libbuild2/variable.cxx +++ b/libbuild2/variable.cxx @@ -3,10 +3,16 @@ #include <libbuild2/variable.hxx> +#include <cstdio> // snprintf() #include <cstring> // memcmp(), memcpy() #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> @@ -584,6 +590,8 @@ namespace build2 if (!wspace (v[0])) { + // Note: see also similar code in to_json_value(). + // int b (v[0] == '0' && (v[1] == 'x' || v[1] == 'X') ? 16 : 10); // May throw invalid_argument or out_of_range. @@ -1501,6 +1509,924 @@ namespace build2 nullptr // Iterate. }; + // json + // + static string + to_string_value (name& n, const char* what) + { + 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; + } + + static json_value + to_json_value (name& n, const char* what) + { + 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.empty ()) + return json_value (string ()); + 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)); + } + // + // Handle the hex notation similar to <uint64_t>::convert() (and JSON5). + // + else if (s[0] == '0' && + (s[1] == 'x' || s[1] == 'X') && + s.size () > 2 && + s.find_first_not_of ("0123456789aAbBcCdDeEfF", 2) == string::npos) + { + return json_value ( + value_traits<uint64_t>::convert (name (move (s)), nullptr), + true /* hex */); + } + else + { + // If this is not a JSON representation of string, array, or object, + // then treat it as a string. + // + // Note that the special `"`, `{`, and `[` characters could be preceded + // with whitespaces. Note: see similar test in json_object below. + // + size_t p (s.find_first_not_of (" \t\n\r")); + + if (p == string::npos || (s[p] != '"' && s[p] != '{' && s[p] != '[')) + return json_value (move (s)); + + // Parse as valid JSON input text. + // +#ifndef BUILD2_BOOTSTRAP + try + { + json_parser p (s, nullptr /* name */); + return json_value (p); + } + catch (const invalid_json_input& e) + { + // Turned out printing line/column/offset can be misleading since we + // could be parsing a single name from a potential list of names. + // feels like without also printing the value this is of not much use. + // +#if 0 + string m ("invalid json input at line "); + m += to_string (e.line); + m += ", column "; + m += to_string (e.column); + m += ", byte offset "; + m += to_string (e.position); + m += ": "; + m += e.what (); +#else + string m ("invalid json input: "); + m += e.what (); +#endif + throw invalid_argument (move (m)); + } +#else + throw invalid_argument ("json parsing requested during bootstrap"); +#endif + } + } + + json_value value_traits<json_value>:: + convert (names&& ns) + { + size_t n (ns.size ()); + + if (n == 0) + { + // Note: this is the ([json] ) case, not ([json] ""). See also the + // relevant note in json_reverse() below. + // + 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.object.reserve (n / 2); + + for (auto i (ns.begin ()); i != ns.end (); ++i) + { + if (!i->pair) + throw invalid_argument ( + "expected pair in json member value '" + to_string (*i) + '\''); + + // Note that we could support JSON-quoted member names but it's + // unclear why would someone want that (and if they do, they can + // always specify JSON text instead). + // + // @@ The empty pair value ([json] one@ ) which is currently empty + // string is inconsistent with empty value ([json] ) above which + // is null. Maybe we could distinguish the one@ and one@"" cases + // via type hints? + // + string n (to_string_value (*i, "json member name")); + json_value v (to_json_value (*++i, "json member")); + + // Check for duplicates. One can use append/prepend to merge. + // + if (find_if (r.object.begin (), r.object.end (), + [&n] (const json_member& m) + { + return m.name == n; + }) != r.object.end ()) + { + throw invalid_argument ( + "duplicate json object member '" + n + '\''); + } + + r.object.push_back (json_member {move (n), move (v)}); + } + + return r; + } + else // array + { + json_value r (json_type::array); + r.array.reserve (n); + + for (name& n: ns) + { + if (n.pair) + throw invalid_argument ( + "unexpected pair in json array element value '" + + to_string (n) + '\''); + + r.array.push_back (to_json_value (n, "json array element")); + } + + return r; + } + } + } + + static void + json_assign (value& v, names&& ns, const variable* var) + { + using traits = value_traits<json_value>; + + try + { + traits::assign (v, traits::convert (move (ns))); + } + catch (const invalid_argument& e) + { + // Note: ns is not guaranteed to be valid. + // + diag_record dr (fail); + dr << "invalid json value"; + + if (var != nullptr) + dr << " in variable " << var->name; + + dr << ": " << e; + } + } + + static void + json_append (value& v, names&& ns, const variable* var) + { + using traits = value_traits<json_value>; + + try + { + traits::append (v, traits::convert (move (ns))); + } + catch (const invalid_argument& e) + { + // Note: ns is not guaranteed to be valid. + // + diag_record dr (fail); + dr << "invalid json value"; + + if (var != nullptr) + dr << " in variable " << var->name; + + dr << ": " << e; + } + } + + static void + json_prepend (value& v, names&& ns, const variable* var) + { + using traits = value_traits<json_value>; + + try + { + traits::prepend (v, traits::convert (move (ns))); + } + catch (const invalid_argument& e) + { + // Note: ns is not guaranteed to be valid. + // + diag_record dr (fail); + dr << "invalid json value"; + + if (var != nullptr) + dr << " in variable " << var->name; + + dr << ": " << e; + } + } + + static names_view + json_reverse (const value& x, names& ns, bool) + { + const json_value& v (x.as<json_value> ()); + + switch (v.type) + { + case json_type::null: + { + // @@ Hm, it would be nice if this somehow got mapped to [null]/empty + // but still be round-trippable to JSON null. Perhaps via type + // hint? + // + // But won't `print ([json] null)` printing nothing be + // surprising. Also, it's not clear that mapping JSON null to out + // [null] is a good idea since our [null] means "no value" while + // JSON null means "null value". + // + // Maybe the current semantics is the best: we map our [null] and + // empty names to JSON null (naturally) but we always reverse JSON + // null to the JSON "null" literal. Or maybe we could reverse it to + // null but type-hint it that it's a spelling or [null]/empty. + // Quite fuzzy, admittedly. In our model null values decay to empty + // so JSON null decaying to "null" literal is strange. Let's try + // and see how it goes. See also json_subscript_impl() below. + // +#if 0 + ns.push_back (name ("null")); +#endif + break; + } + case json_type::boolean: + { + ns.push_back (name (v.boolean ? "true" : "false")); + break; + } + case json_type::signed_number: + { + ns.push_back (value_traits<int64_t>::reverse (v.signed_number)); + break; + } + case json_type::unsigned_number: + { + ns.push_back (value_traits<uint64_t>::reverse (v.unsigned_number)); + break; + } + case json_type::hexadecimal_number: + { + // Hexadecimal representation of 64-bit integers requires a maximum of + // 10 character (plus '\0'): 0xffffffff. + // + char buf[11]; + snprintf (buf, sizeof (buf), + "0x%llx", + static_cast<unsigned long long> (v.unsigned_number)); + + ns.push_back (name (string (buf))); + break; + } + case json_type::string: + // + // @@ Hm, it would be nice if this somehow got mapped to unquoted + // string but still be round-trippable to JSON value. Perhaps via + // the type hint idea? This is pretty bad. See also subscript we + // hacked around this somewhat. + // + // Note that it may be tempting to fix this by only quoting strings + // that would otherwise be mis-interpreted (null, true, all digits, + // etc). But that would be worse: things would seem to work but + // fall apart in the perhaps unlikely event of encountering one of + // the problematic values. It is better to produce a consistent + // result. + // + case json_type::array: + case json_type::object: + { + // Serialize as JSON output text. + // + string o; + +#ifndef BUILD2_BOOTSTRAP + try + { + // Disable pretty-printing so that the output is all on the same + // line. While it's not going to be easy to read for larger JSON + // outputs, it will fit better into the existing model where none of + // the value representations use formatting newlines. If a pretty- + // printed representation is required, then the $json.serialize() + // function can be used to obtain it. + // + json_buffer_serializer s (o, 0 /* indentation */); + v.serialize (s); + } + catch (const invalid_json_output& e) + { + // Note: the same diagnostics as in $json.serialize(). + // + diag_record dr; + dr << fail << "invalid json value: " << e; + + if (e.event) + dr << info << "while serializing " << to_string (*e.event); + + if (e.offset != string::npos) + dr << info << "offending byte offset " << e.offset; + } +#else + fail << "json serialization requested during bootstrap"; +#endif + ns.push_back (name (move (o))); + break; + } + } + + return ns; + } + + static int + json_compare (const value& l, const value& r) + { + return l.as<json_value> ().compare (r.as<json_value> ()); + } + + // Return null value if the index/name is out of range. + // + static value + json_subscript_impl (const value& val, value* val_data, + uint64_t i, const string& n, bool index) + { + const json_value& jv (val.as<json_value> ()); + + json_value jr; + + if (index) + { + if (i >= (jv.type == json_type::array ? jv.array.size () : + jv.type == json_type::object ? jv.object.size () : 1)) + return value (); + + switch (jv.type) + { + case json_type::null: + return value (); // JSON null has no elements. + case json_type::boolean: + case json_type::signed_number: + case json_type::unsigned_number: + case json_type::hexadecimal_number: + case json_type::string: + { + // Steal the value if possible. + // + jr = (&val == val_data + ? json_value (move (const_cast<json_value&> (jv))) + : json_value (jv)); + break; + } + case json_type::array: + { + // Steal the value if possible. + // + const json_value& r (jv.array[i]); + jr = (&val == val_data + ? json_value (move (const_cast<json_value&> (r))) + : json_value (r)); + break; + } + case json_type::object: + { + // Represent as an object with one member. + // + new (&jr.object) json_value::object_type (); + jr.type = json_type::object; + + // Steal the member if possible. + // + const json_member& m (jv.object[i]); + jr.object.push_back (&val == val_data + ? json_member (move (const_cast<json_member&> (m))) + : json_member (m)); + break; + } + } + } + else + { + auto i (find_if (jv.object.begin (), + jv.object.end (), + [&n] (const json_member& m) + { + return m.name == n; + })); + + if (i == jv.object.end ()) + return value (); + + // Steal the member value if possible. + // + jr = (&val == val_data + ? json_value (move (const_cast<json_value&> (i->value))) + : json_value (i->value)); + } + + // @@ As a temporary work around for the lack of type hints (see + // json_reverse() for background), reverse simple JSON values to the + // corresponding fundamental type values. The thinking here is that + // subscript (and iteration) is primarily meant for consumption (as + // opposed to reverse() where it is used to build up values and thus + // needs things to be fully reversible). Once we add type hints, then + // this should become unnecessary and we should be able to just always + // return json_value. + // + // @@ TODO: split this function into two (index/name) once get rid of this. + // +#if 1 + switch (jr.type) + { + case json_type::null: return value (names {}); + case json_type::boolean: return value (jr.boolean); + case json_type::signed_number: return value (jr.signed_number); + case json_type::unsigned_number: + case json_type::hexadecimal_number: return value (jr.unsigned_number); + case json_type::string: return value (move (jr.string)); + case json_type::array: + case json_type::object: break; + } +#endif + + return value (move (jr)); + } + + static value + json_subscript (const value& val, value* val_data, + value&& sub, + const location& sloc, + const location& bloc) + { + const json_value* jv (val.null ? nullptr : &val.as<json_value> ()); + + // For consistency with other places treat JSON null value as maybe + // missing array/object. In particular, we don't want to fail trying to + // lookup by-name on a null value which could have been an object. + // + if (jv != nullptr && jv->type == json_type::null) + jv = nullptr; + + // Process subscript even if the value is null to make sure it is valid. + // + bool index; + uint64_t i (0); + string n; + + // Always interpret uint64-typed subscript as index even for objects. + // This can be used to, for example, to iterate with an index over object + // members. + // + if (!sub.null && sub.type == &value_traits<uint64_t>::value_type) + { + i = sub.as<uint64_t> (); + index = true; + } + else + { + // How we interpret the subscript depends on the JSON value type. For + // objects we treat it as a string (member name) and for everything else + // as an index. + // + // What if the value is null and we don't have a JSON type? In this case + // we treat as a string since a valid number is also a valid string. + // + try + { + if (jv == nullptr || jv->type == json_type::object) + { + n = convert<string> (move (sub)); + index = false; + } + else + { + i = convert<uint64_t> (move (sub)); + index = true; + } + } + catch (const invalid_argument& e) + { + // We will likely be trying to interpret a member name as an integer + // due to the incorrect value type so issue appropriate diagnostics. + // + diag_record dr; + dr << fail (sloc) << "invalid json value subscript: " << e; + + if (jv != nullptr && jv->type != json_type::object) + dr << info << "json value type is " << jv->type; + + dr << info (bloc) << "use the '\\[' escape sequence if this is a " + << "wildcard pattern" << endf; + } + } + + return (jv != nullptr + ? json_subscript_impl (val, val_data, i, n, index) + : value ()); + } + + void json_iterate (const value& val, + const function<void (value&&, bool first)>& f) + { + // Implement in terms of subscript for consistency (in particular, + // iterating over simple values like number, string). + // + for (uint64_t i (0);; ++i) + { + value e (json_subscript_impl (val, nullptr, i, {}, true)); + + if (e.null) + break; + + f (move (e), i == 0); + } + } + + 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>, + &json_assign, + json_append, + json_prepend, + &json_reverse, + nullptr, // No cast (cast data_ directly). + &json_compare, + &default_empty<json_value>, + &json_subscript, + &json_iterate + }; + + // json_array + // + json_array value_traits<json_array>:: + convert (names&& ns) + { + json_array r; + + size_t n (ns.size ()); + if (n == 0) + ; // Empty. + else if (n == 1) + { + // Tricky: this can still be JSON input text that is an array. And if + // it's not, then make it an element of an array. + // + json_value v (to_json_value (ns.front (), "json")); + + if (v.type == json_type::array) + r.array = move (v.array); + else + r.array.push_back (move (v)); + } + else + { + r.array.reserve (n); + + for (name& n: ns) + { + if (n.pair) + throw invalid_argument ( + "unexpected pair in json array element value '" + + to_string (n) + '\''); + + r.array.push_back (to_json_value (n, "json array element")); + } + } + + return r; + } + + static void + json_array_assign (value& v, names&& ns, const variable* var) + { + using traits = value_traits<json_array>; + + try + { + traits::assign (v, traits::convert (move (ns))); + } + catch (const invalid_argument& e) + { + // Note: ns is not guaranteed to be valid. + // + diag_record dr (fail); + dr << "invalid json array"; + + if (var != nullptr) + dr << " in variable " << var->name; + + dr << ": " << e; + } + } + + static void + json_array_append (value& v, names&& ns, const variable* var) + { + using val_traits = value_traits<json_value>; + using arr_traits = value_traits<json_array>; + + try + { + arr_traits::append (v, val_traits::convert (move (ns))); + } + catch (const invalid_argument& e) + { + // Note: ns is not guaranteed to be valid. + // + diag_record dr (fail); + dr << "invalid json array"; + + if (var != nullptr) + dr << " in variable " << var->name; + + dr << ": " << e; + } + } + + static void + json_array_prepend (value& v, names&& ns, const variable* var) + { + using val_traits = value_traits<json_value>; + using arr_traits = value_traits<json_array>; + + try + { + arr_traits::prepend (v, val_traits::convert (move (ns))); + } + catch (const invalid_argument& e) + { + // Note: ns is not guaranteed to be valid. + // + diag_record dr (fail); + dr << "invalid json array"; + + if (var != nullptr) + dr << " in variable " << var->name; + + dr << ": " << e; + } + } + + const json_array value_traits<json_array>::empty_instance; + const char* const value_traits<json_array>::type_name = "json_array"; + + const value_type value_traits<json_array>::value_type + { + type_name, + sizeof (json_array), + &value_traits<json_value>::value_type, // Base (assuming direct cast works + // for both). + true, // Container. + &value_traits<json_value>::value_type, // Element (json_value). + &default_dtor<json_array>, + &default_copy_ctor<json_array>, + &default_copy_assign<json_array>, + &json_array_assign, + &json_array_append, + &json_array_prepend, + &json_reverse, + nullptr, // No cast (cast data_ directly). + &json_compare, + &default_empty<json_array>, + &json_subscript, + &json_iterate + }; + + // json_object + // + json_object value_traits<json_object>:: + convert (names&& ns) + { + json_object r; + + size_t n (ns.size ()); + if (n == 0) + ; // Empty. + else if (n == 1) + { + // Tricky: this can still be JSON input text that is an object. So do + // a similar check as in to_json_value() above. + // + name& n (ns.front ()); + + if (!n.simple () || n.pattern) + throw_invalid_argument (n, nullptr, "json object"); + + string& s (n.value); + size_t p (s.find_first_not_of (" \t\n\r")); + + if (p == string::npos || s[p] != '{') + { + // Unlike for array above, we cannot turn any value into a member. + // + throw invalid_argument ("expected json object instead of '" + s + '\''); + } + + json_value v (to_json_value (ns.front (), "json object")); + assert (v.type == json_type::object); + r.object = move (v.object); + } + else + { + r.object.reserve (n / 2); + + for (auto i (ns.begin ()); i != ns.end (); ++i) + { + if (!i->pair) + throw invalid_argument ( + "expected pair in json member value '" + to_string (*i) + '\''); + + string n (to_string_value (*i, "json member name")); + json_value v (to_json_value (*++i, "json member")); + + if (find_if (r.object.begin (), r.object.end (), + [&n] (const json_member& m) + { + return m.name == n; + }) != r.object.end ()) + { + throw invalid_argument ( + "duplicate json object member '" + n + '\''); + } + + r.object.push_back (json_member {move (n), move (v)}); + } + } + + return r; + } + + static void + json_object_assign (value& v, names&& ns, const variable* var) + { + using traits = value_traits<json_object>; + + try + { + traits::assign (v, traits::convert (move (ns))); + } + catch (const invalid_argument& e) + { + // Note: ns is not guaranteed to be valid. + // + diag_record dr (fail); + dr << "invalid json object"; + + if (var != nullptr) + dr << " in variable " << var->name; + + dr << ": " << e; + } + } + + static void + json_object_append (value& v, names&& ns, const variable* var) + { + using val_traits = value_traits<json_value>; + using obj_traits = value_traits<json_object>; + + try + { + obj_traits::append (v, val_traits::convert (move (ns))); + } + catch (const invalid_argument& e) + { + // Note: ns is not guaranteed to be valid. + // + diag_record dr (fail); + dr << "invalid json object"; + + if (var != nullptr) + dr << " in variable " << var->name; + + dr << ": " << e; + } + } + + static void + json_object_prepend (value& v, names&& ns, const variable* var) + { + using val_traits = value_traits<json_value>; + using obj_traits = value_traits<json_object>; + + try + { + obj_traits::prepend (v, val_traits::convert (move (ns))); + } + catch (const invalid_argument& e) + { + // Note: ns is not guaranteed to be valid. + // + diag_record dr (fail); + dr << "invalid json object"; + + if (var != nullptr) + dr << " in variable " << var->name; + + dr << ": " << e; + } + } + + const json_object value_traits<json_object>::empty_instance; + const char* const value_traits<json_object>::type_name = "json_object"; + + const value_type value_traits<json_object>::value_type + { + type_name, + sizeof (json_object), + &value_traits<json_value>::value_type, // Base (assuming direct cast works + // for both). + true, // Container. + &value_traits<json_value>::value_type, // Element (json_value). + &default_dtor<json_object>, + &default_copy_ctor<json_object>, + &default_copy_assign<json_object>, + &json_object_assign, + &json_object_append, + &json_object_prepend, + &json_reverse, + nullptr, // No cast (cast data_ directly). + &json_compare, + &default_empty<json_object>, + &json_subscript, + &json_iterate + }; + // cmdline // cmdline value_traits<cmdline>:: diff --git a/libbuild2/variable.hxx b/libbuild2/variable.hxx index 9d7b001..6b0b30e 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> @@ -1201,6 +1203,65 @@ namespace build2 static const map_value_type<K, V> value_type; }; + // json + // + // Note that we do not expose json_member as a value type instead + // representing it as an object with one member. While we could expose + // member (and reverse it as a pair since there is no valid JSON + // representation for a standalone member), this doesn't seem to buy us much + // but will cause complications (for example, in supporting append/prepend). + // On the other hand, representing a member as an object only requires a bit + // of what looks like harmless looseness in a few contexts (such as the + // $json.member_*() functions). + // + 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 array/object + + static const json_value empty_instance; // null + static const char* const type_name; + static const build2::value_type value_type; + }; + + template <> + struct LIBBUILD2_SYMEXPORT value_traits<json_array> + { + static_assert (sizeof (json_array) <= value::size_, "insufficient space"); + + static json_array convert (names&&); + static void assign (value&, json_array&&); + static void append (value&, json_value&&); // Note: value, not array. + static void prepend (value&, json_value&&); + static bool empty (const json_array& v) {return v.array.empty ();} + + static const json_array empty_instance; // empty array + static const char* const type_name; + static const build2::value_type value_type; + }; + + template <> + struct LIBBUILD2_SYMEXPORT value_traits<json_object> + { + static_assert (sizeof (json_object) <= value::size_, "insufficient space"); + + static json_object convert (names&&); + static void assign (value&, json_object&&); + static void append (value&, json_value&&); // Note: value, not object. + static void prepend (value&, json_value&&); + static bool empty (const json_object& v) {return v.object.empty ();} + + static const json_object empty_instance; // empty object + 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..b8f80e3 100644 --- a/libbuild2/variable.ixx +++ b/libbuild2/variable.ixx @@ -906,6 +906,113 @@ namespace build2 new (&v.data_) map<K, V> (move (x)); } + // json + // + inline bool value_traits<json_value>:: + empty (const json_value& v) + { + // Note: should be consistent with $json.size(). + // + switch (v.type) + { + case json_type::null: return true; + case json_type::boolean: + case json_type::signed_number: + case json_type::unsigned_number: + case json_type::hexadecimal_number: + case json_type::string: break; + case json_type::array: return v.array.empty (); + case json_type::object: return v.object.empty (); + } + + return false; + } + + inline 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)); + } + + inline void value_traits<json_value>:: + append (value& v, json_value&& x) + { + if (v) + v.as<json_value> ().append (move (x)); + else + new (&v.data_) json_value (move (x)); + } + + inline void value_traits<json_value>:: + prepend (value& v, json_value&& x) + { + if (v) + v.as<json_value> ().prepend (move (x)); + else + new (&v.data_) json_value (move (x)); + } + + // json_array + // + inline void value_traits<json_array>:: + assign (value& v, json_array&& x) + { + if (v) + v.as<json_array> () = move (x); + else + new (&v.data_) json_array (move (x)); + } + + inline void value_traits<json_array>:: + append (value& v, json_value&& x) + { + if (!v) + new (&v.data_) json_array (); + + v.as<json_array> ().append (move (x)); + } + + inline void value_traits<json_array>:: + prepend (value& v, json_value&& x) + { + if (!v) + new (&v.data_) json_array (); + + v.as<json_array> ().prepend (move (x)); + } + + // json_object + // + inline void value_traits<json_object>:: + assign (value& v, json_object&& x) + { + if (v) + v.as<json_object> () = move (x); + else + new (&v.data_) json_object (move (x)); + } + + inline void value_traits<json_object>:: + append (value& v, json_value&& x) + { + if (!v) + new (&v.data_) json_object (); + + v.as<json_object> ().append (move (x)); + } + + inline void value_traits<json_object>:: + prepend (value& v, json_value&& x) + { + if (!v) + new (&v.data_) json_object (); + + v.as<json_object> ().prepend (move (x)); + } + // variable_pool // inline const variable* variable_pool:: diff --git a/tests/function/builtin/testscript b/tests/function/builtin/testscript index 88f802a..04e8bd8 100644 --- a/tests/function/builtin/testscript +++ b/tests/function/builtin/testscript @@ -53,6 +53,9 @@ $* <'print $empty(abc)' >'false' : name $* <'print $empty(abc cxx{foo})' >'false' : names $* <'print $empty([bool] false)' >'false' : bool + $* <'print $empty([json] null)' >'true' : json-null + $* <'print $empty([json] "[]")' >'true' : json-array + $* <'print $empty([json] "{}")' >'true' : json-object } : first-second diff --git a/tests/function/json/buildfile b/tests/function/json/buildfile new file mode 100644 index 0000000..45c60d2 --- /dev/null +++ b/tests/function/json/buildfile @@ -0,0 +1,4 @@ +# file : tests/function/json/buildfile +# license : MIT; see accompanying LICENSE file + +./: testscript $b diff --git a/tests/function/json/testscript b/tests/function/json/testscript new file mode 100644 index 0000000..f5fc671 --- /dev/null +++ b/tests/function/json/testscript @@ -0,0 +1,224 @@ +# file : tests/function/json/testscript +# license : MIT; see accompanying LICENSE file + +# See also tests in type/json/. + +.include ../../common.testscript + +: type +: +$* <<EOI >>EOO +print $value_type([json] ) +print $value_type([json] null) +print $value_type([json] true) +print $value_type([json] 123) +print $value_type([json] -123) +print $value_type([json] 123, true) +print $value_type([json] -123, true) +print $value_type([json] 1 2 3) +print $value_type([json] one@1 two@2 three@3) + +j = [json] one@1 two@2 three@3 +i = [uint64] 1 +m = ($j[$i]) +print $value_type($j[$i]) +print $value_type($m) +EOI +null +null +boolean +number +number +unsigned number +signed number +array +object +object +object +EOO + +: member +: +$* <<EOI >>EOO +j = [json] one@1 two@2 three@3 +i = [uint64] 1 +m = ($j[$i]) +print $member_name($j[$i]) $member_value($j[$i]) +print $member_name($m) $member_value($m) +for m: $j + print $member_name($m) $member_value($m) +EOI +two 2 +two 2 +one 1 +two 2 +three 3 +EOO + +: size +: +$* <<EOI >>EOO +print $size([json] null) +print $size([json] true) +print $size([json] 123) +print $size([json] abc) +print $size([string] ([json] abc)) # @@ Should be 3 (quoted, type hint). +print $size([json] 1 2 3) +print $size([json] one@1 two@2 three@3) +EOI +0 +1 +1 +1 +5 +3 +3 +EOO + +: find +: +$* <<EOI >>EOO +j = [json] 1 ([json] one@1 two@2) 2 true 3 null 4 abc -5 null ([json] 1 2 3) +print $find_index($j, null) +print $find_index($j, true) +print $find_index($j, 3) +print $find_index($j, 0x4) +print $find_index($j, -5) +print $find_index($j, abc) +print $find_index($j, [json] 1 2 3) +print $find_index($j, [json] two@2 one@1) +print $find_index($j, [json] 1 2) +print $find_index($j, [json] one@1) +print $find_index($j, [json] one@1 two@2 three@3) +print $find_index($j, [json] one@1 TWO@3) +print $find_index($j, [json] one@1 two@3) +EOI +5 +3 +4 +6 +8 +7 +10 +1 +11 +11 +11 +11 +11 +EOO + +: parse +: +{ + : basics + : + $* <<EOI >>EOO + print $json.parse('[123, "abc", {"one":1, "two":2}]') + EOI + [123,"abc",{"one":1,"two":2}] + EOO + + : diagnostics-invalid-input + : + $* <<EOI 2>>EOE != 0 + print $json.parse('{"one":, "two":2}]') + EOI + error: invalid json input: unexpected byte ',' in value + info: line 1, column 8, byte offset 8 + <stdin>:1:8: info: while calling json.parse(<untyped>) + EOE + + : diagnostics-duplicate-input + : + $* <<EOI 2>>EOE != 0 + print $json.parse('{"one":1, "one":2}]') + EOI + error: invalid json input: duplicate object member 'one' + info: line 1, column 11, byte offset 15 + <stdin>:1:8: info: while calling json.parse(<untyped>) + EOE +} + +: serialize +: +{ + : basics + : + $* <<EOI >>EOO + j = [json] 123 abc ([json] one@1 two@2) + print $json.serialize($j) + print $json.serialize($j, 0) + EOI + [ + 123, + "abc", + { + "one": 1, + "two": 2 + } + ] + [123,"abc",{"one":1,"two":2}] + EOO + + : diagnostics + : + if false + { + # This is not easy to trigger normally so we have a normally-disabled + # special hack in the $json.serialize() implementation to trigger this. + # + $* <<EOI 2>>EOE != 0 + print $json.serialize([json] deadbeef) + EOI + error: invalid json value: invalid UTF-8 text + info: while serializing string value + info: offending byte offset 4 + <stdin>:1:8: info: while calling json.serialize(json) + EOE + } + +} + +: load +: +{ + : basics + : + cat <<EOI >=input.json; + { + "str":"abc", + "num":123, + "arr":[1, 2, 3], + "obj":{"one":1, "two":2, "three":3} + } + EOI + $* <<EOI >>EOO + j = $json.load(input.json) + for m: $j + print $member_name($m) $member_value($m) + EOI + str "abc" + num 123 + arr [1,2,3] + obj {"one":1,"two":2,"three":3} + EOO + + : diagnostics + : + cat <<EOI >=input.json; + { + "str":"abc", + "num":, + "arr":[1, 2, 3], + "obj":{"one":1, "two":2, "three":3} + } + EOI + $* <<EOI 2>>EOE != 0 + j = $json.load(input.json) + EOI + input.json:3:9: error: invalid json input: unexpected byte ',' in value + info: byte offset 26 + <stdin>:1:6: info: while calling json.load(<untyped>) + EOE +} diff --git a/tests/type/json/buildfile b/tests/type/json/buildfile new file mode 100644 index 0000000..5bc6bf2 --- /dev/null +++ b/tests/type/json/buildfile @@ -0,0 +1,4 @@ +# file : tests/type/json/buildfile +# license : MIT; see accompanying LICENSE file + +./: testscript $b diff --git a/tests/type/json/testscript b/tests/type/json/testscript new file mode 100644 index 0000000..d9827df --- /dev/null +++ b/tests/type/json/testscript @@ -0,0 +1,416 @@ +# file : tests/type/json/testscript +# license : MIT; see accompanying LICENSE file + +# See also tests in function/json/. + +.include ../../common.testscript + +: basics +: +{ + : empty-null + : + $* <<EOI >>EOO + print ([json, null] ) + print ([json] null) + print ([json] ) + print ([json] "") + print ([json_array] ) + print ([json_object] ) + print ([json] one@null) + print ([json] one@) # @@ Would be more consistent if were null (type hints?) + print ([json] one@"") + EOI + [null] + + + "" + [] + {} + {"one":null} + {"one":""} + {"one":""} + EOO + + : reverse + : + $* <<EOI >>EOO + print ([json] null) + print ([json] true) + print ([json] 123) + print ([json] -123) + print ([json] 0xdecaf) + print ([json] abc) # @@ Ideally we would like this to be reversed unquoted. + print ([json] '"abc"') # @@ Ditto. + print (([json] abc)[0]) # @@ Workaround. + print ([json] dir/{file1 file2}) + print ([json] ' ["dir/file1", "dir/file2"] ') + print ([json] zero@null one@1 two@abc three@([json] x@123 y@-123) four@([json] null true)) + print ([json] '{"zero":null,"one":1,"two":"abc","three":{"x":123,"y":-123},"four":[null,true]}') + EOI + + true + 123 + -123 + 0xdecaf + "abc" + "abc" + abc + ["dir/file1","dir/file2"] + ["dir/file1","dir/file2"] + {"zero":null,"one":1,"two":"abc","three":{"x":123,"y":-123},"four":[null,true]} + {"zero":null,"one":1,"two":"abc","three":{"x":123,"y":-123},"four":[null,true]} + EOO + + + : hex + : + $* <<EOI >>EOO + print ([json] 0xffffFFFF) + + # These should be in the hexadecimal notation once we switch to JSON5. + # + print ([json] 0x0 0x01 0xff 0xFFFF) + print ([json] ff@0xff FFFF@0xFFFF) + + # @@ This should start working once we switch to type hints in subscript. + # + #j = [json] ff@0xff + #print $value_type($j[ff], true) + print 'hexadecimal number' + EOI + 0xffffffff + [0,1,255,65535] + {"ff":255,"FFFF":65535} + hexadecimal number + EOO + + : diagnostics-reverse-invalid + : + $* <<EOI 2>>EOE != 0 + o = [json] '{"one":1, "two":}' + EOI + error: invalid json value in variable o: invalid json input: unexpected byte '}' in value + EOE + + : diagnostics-duplicate-member + : + $* <<EOI 2>>EOE != 0 + o = [json] one@1 one@2 + EOI + error: invalid json value in variable o: duplicate json object member 'one' + EOE +} + +: compare +: +{ + : type + : + $* <<EOI >>EOO + print (([json] null) < ([json] true)) + print (([json] true) < ([json] 0)) + print (([json] 123) < ([json] '"0"')) + print (([json] abc) < ([json] xxx yyy)) + print (([json] xxx yyy) < ([json] xxx@null yyy@null)) + EOI + true + true + true + true + true + EOO + + : simple + : + $* <<EOI >>EOO + print (([json] false) == ([json] false)) + print (([json] false) < ([json] true)) + + print (([json] 123) == ([json] 123)) + print (([json] -123) == ([json] -123)) + print (([json] 0xff) == ([json] 255)) + print (([json] 0) == ([json] -0)) + print (([json] -1) < ([json] 0)) + print (([json] 123) < ([json] 234)) + print (([json] -234) < ([json] -123)) + + print (([json] abc) == ([json] abc)) + print (([json] abc) < ([json] abz)) + print (([json] abc) < ([json] abcd)) + EOI + true + true + true + true + true + true + true + true + true + true + true + true + EOO + + : array + : + $* <<EOI >>EOO + print (([json] 1 2 3) == ([json] 1 2 3)) + print (([json] 1 2 3) < ([json] 1 2 4)) + print (([json] 1 2 3) < ([json] 1 2 3 4)) + EOI + true + true + true + EOO + + : object + : + $* <<EOI >>EOO + print (([json] one@1 two@2 three@3) == ([json] three@3 one@1 two@2)) + print (([json] one@1 two@2 three@3) < ([json] three@3 one@1 two@4)) + print (([json] one@1 three@3) < ([json] three@3 one@1 two@2)) + EOI + true + true + true + EOO +} + +: append-prepend +: +{ + : array + : + $* <<EOI >'[0,1,2,3,4,5,6,7,8]' + a = [json] 2 3 + a += 4 + a += 5 6 + a += [json] 7 8 + a =+ [json] 0 1 + print $a + EOI + + : array-type + : + $* <<EOI >'[1,2,3,4,5]' + [json_array] a = + a += 1 + a += 2 3 + a += [json_array] 4 5 # @@ Should be possible to use json. + print $a + EOI + + : object + : + $* <<EOI >'{"zero":0,"one":6,"two":8,"three":9,"four":4,"five":5,"seven":7}' + o = [json] one@1 two@2 three@3 + o += four@4 + o += five@5 one@6 + o += [json] seven@7 two@8 + o =+ [json] zero@0 three@9 + print $o + EOI + + : object-type + : + $* <<EOI >'{"one":1,"two":2,"three":3,"four":4,"five":5}' + [json_object] o = + o += one@1 + o += two@2 three@3 + o += [json_object] four@4 five@5 # @@ Should be possible to use json. + print $o + EOI + + : boolean + : + $* <<EOI >>EOO + b = [json] false + b += [json] true + print $b + EOI + true + EOO + + : number + : + $* <<EOI >>EOO + n = [json] -2 + print $value_type($n, true) $n + n += 1 + print $value_type($n, true) $n + n += 1 + print $value_type($n, true) $n + n += 1 + print $value_type($n, true) $n + n += [json] -1 + print $value_type($n, true) $n + n += [json] -1 + print $value_type($n, true) $n + EOI + signed number -2 + signed number -1 + unsigned number 0 + unsigned number 1 + unsigned number 0 + signed number -1 + EOO + + : string + : + $* <<EOI >>EOO + s = [json] yyy + s += [json] zzz + s =+ [json] xxx + print $s + EOI + "xxxyyyzzz" + EOO + + : invalid + : + $* <<EOI 2>>EOE != 0 + a = [json] 1 2 3 + s = [json] str + s += $a + print $s + EOI + error: invalid json value in variable s: unable to append array to string + EOE +} + +: subscript +: +{ + : null + : + $* <<EOI >>EOO + j = [json] null + print ($j[0]) + print ($j[one]) + EOI + [null] + [null] + EOO + + : array + : + $* <<EOI >>EOO + j = [json] 1 2 3 null + print ($j[1]) + print ($j[3]) + print ($j[4]) + EOI + 2 + + [null] + EOO + + : object-name + : + $* <<EOI >>EOO + j = [json] one@1 two@2 three@3 four@null + print ($j[two]) + print ($j[four]) + print ($j[five]) + EOI + 2 + + [null] + EOO + + : object-index + : + $* <<EOI >>EOO + j = [json] one@1 two@2 three@3 + print ($j[([uint64] 1)]) + EOI + {"two":2} + EOO + + : reverse + : + $* <<EOI >>EOO + print (([json] one@null)[one]) + print (([json] one@true)[one]) + print (([json] one@123)[one]) + print (([json] one@-123)[one]) + print (([json] one@0xdecaf)[one]) + print (([json] one@abc)[one]) + EOI + + true + 123 + -123 + 912559 + abc + EOO + + : diagnostics-not-object + : + $* <<EOI 2>>EOE != 0 + j = [json] 1 2 3 + print ($j[one]) + EOI + <stdin>:2:11: error: invalid json value subscript: invalid uint64 value 'one' + info: json value type is array + <stdin>:2:9: info: use the '\[' escape sequence if this is a wildcard pattern + EOE +} + +: iteration +: +{ + : null + : + $* <<EOI + for v: ([json] null) + print $v + EOI + + : simple + : + $* <<EOI >>EOO + for v: ([json] 123) + print $v + EOI + 123 + EOO + + : array + : + $* <<EOI >>EOO + for v: ([json] 1 2 3) + print $v + EOI + 1 + 2 + 3 + EOO + + : object + : + $* <<EOI >>EOO + for v: ([json] one@1 two@2 three@3) + print $v + EOI + {"one":1} + {"two":2} + {"three":3} + EOO + + : reverse + : + $* <<EOI >>EOO + for v: ([json] null true 123 -123 0xdecaf abc) + print $v + EOI + + true + 123 + -123 + 912559 + abc + EOO +} |