From 36d6b4e5549dc45baf890105de5ef487211f0144 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Tue, 6 Feb 2024 05:22:12 +0200 Subject: Add experimental support for JSON value types New types: json json_array json_object New functions: $json.value_type() $json.value_size() $json.member_{name,value}() $json.object_names() $json.array_size() $json.array_find(, ) $json.array_find_index(, ) $json.load() $json.parse() $json.serialize([, ]) 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) --- libbuild2/functions-json.cxx | 240 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 libbuild2/functions-json.cxx (limited to 'libbuild2/functions-json.cxx') 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 +#include + +#ifndef BUILD2_BOOTSTRAP +# include +# include +#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 (move (v)))); + return i != e ? i - b : a.array.size (); + }; + + void + json_functions (function_map& m) + { + function_family f (m, "json"); + + // $value_type([, ]) + // + // Return the type of a JSON value: `null`, `boolean`, `number`, `string`, + // `array`, or `object`. If the argument is `true`, + // then instead of `number` return `signed number`, `unsigned number`, or + // `hexadecimal number`. + // + f["value_type"] += [] (json_value v, optional distinguish_numbers) + { + bool dn (distinguish_numbers && + convert (move (*distinguish_numbers))); + + return to_string (v.type, dn); + }; + + // $member_name() + // + // 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() + // + // 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() + // + // 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::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(, ) + // + // 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(, ) + // + // 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() + // + // 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 (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() + // + // 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 (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([, ]) + // + // Serialize the specified JSON value and return the resulting JSON output + // text. + // + // The optional 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 indentation) + { + uint64_t i (indentation ? convert (*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 + } +} -- cgit v1.1