summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrad King <brad.king@kitware.com>2020-09-17 13:46:19 +0000
committerKitware Robot <kwrobot@kitware.com>2020-09-17 09:47:43 -0400
commitbb8afa0020759ee79149c89ca178236cb85adfb5 (patch)
treeae6b990951d2d61d9517284e0265ae36b68385b9
parent5e53b58c4a0d2a859b8be406fbe9d2e4d5463ddd (diff)
parent8eab76eb846f6aabf6a814574573f16dd992832a (diff)
downloadcmake-bb8afa0020759ee79149c89ca178236cb85adfb5.tar.gz
Merge topic 'string-json-support'
8eab76eb84 string(JSON): Adds JSON parsing support to the string command Acked-by: Kitware Robot <kwrobot@kitware.com> Acked-by: Ben Boeckel <ben.boeckel@kitware.com> Acked-by: Cristian Adam <cristian.adam@gmail.com> Acked-by: Michael Hirsch, Ph.D. <michael@scivision.dev> Merge-request: !5159
-rw-r--r--Help/command/string.rst103
-rw-r--r--Help/release/dev/string-json-support.rst5
-rw-r--r--Source/cmStringCommand.cxx300
-rw-r--r--Tests/RunCMake/string/JSON.cmake342
-rw-r--r--Tests/RunCMake/string/JSONNoArgs-result.txt1
-rw-r--r--Tests/RunCMake/string/JSONNoArgs-stderr.txt4
-rw-r--r--Tests/RunCMake/string/JSONNoArgs.cmake1
-rw-r--r--Tests/RunCMake/string/JSONNoJson-result.txt1
-rw-r--r--Tests/RunCMake/string/JSONNoJson-stderr.txt4
-rw-r--r--Tests/RunCMake/string/JSONNoJson.cmake1
-rw-r--r--Tests/RunCMake/string/JSONOneArg-result.txt1
-rw-r--r--Tests/RunCMake/string/JSONOneArg-stderr.txt4
-rw-r--r--Tests/RunCMake/string/JSONOneArg.cmake1
-rw-r--r--Tests/RunCMake/string/JSONWrongMode-result.txt1
-rw-r--r--Tests/RunCMake/string/JSONWrongMode-stderr.txt5
-rw-r--r--Tests/RunCMake/string/JSONWrongMode.cmake1
-rw-r--r--Tests/RunCMake/string/RunCMakeTest.cmake7
-rw-r--r--Tests/RunCMake/string/json/unicode.json8
18 files changed, 790 insertions, 0 deletions
diff --git a/Help/command/string.rst b/Help/command/string.rst
index cfcf9147fa..0bc2b484d9 100644
--- a/Help/command/string.rst
+++ b/Help/command/string.rst
@@ -43,6 +43,19 @@ Synopsis
string(`TIMESTAMP`_ <out-var> [<format string>] [UTC])
string(`UUID`_ <out-var> ...)
+ `JSON`_
+ string(JSON <out-var> [ERROR_VARIABLE <error-var>]
+ {`GET`_ | `TYPE`_ | :ref:`LENGTH <JSONLENGTH>` | `REMOVE`_}
+ <json-string> <member|index> [<member|index> ...])
+ string(JSON <out-var> [ERROR_VARIABLE <error-var>]
+ `MEMBER`_ <json-string>
+ [<member|index> ...] <index>)
+ string(JSON <out-var> [ERROR_VARIABLE <error-var>]
+ `SET`_ <json-string>
+ <member|index> [<member|index> ...] <value>)
+ string(JSON <out-var> [ERROR_VARIABLE <error-var>]
+ `EQUAL`_ <json-string1> <json-string2>)
+
Search and Replace
^^^^^^^^^^^^^^^^^^
@@ -470,3 +483,93 @@ A UUID has the format ``xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx``
where each ``x`` represents a lower case hexadecimal character.
Where required, an uppercase representation can be requested
with the optional ``UPPER`` flag.
+
+JSON
+^^^^
+
+.. _JSON:
+
+Functionality for querying a JSON string
+
+.. _GET:
+.. code-block:: cmake
+
+ string(JSON <out-var> [ERROR_VARIABLE <error-variable>]
+ GET <json-string> <member|index> [<member|index> ...])
+
+Get an element from ``<json-string>`` at the location given
+by the list of ``<member|index>`` arguments.
+Array and object elements will be returned as a JSON string.
+Boolean elements will be returned as ``ON`` or ``OFF``.
+Null elements will be returned as an empty string.
+Number and string types will be returned as strings.
+
+.. _TYPE:
+.. code-block:: cmake
+
+ string(JSON <out-var> [ERROR_VARIABLE <error-variable>]
+ TYPE <json-string> <member|index> [<member|index> ...])
+
+Get the type of an element in ``<json-string>`` at the location
+given by the list of ``<member|index>`` arguments. The ``<out-var>``
+will be set to one of ``NULL``, ``NUMBER``, ``STRING``, ``BOOLEAN``,
+``ARRAY``, or ``OBJECT``.
+
+.. _MEMBER:
+.. code-block:: cmake
+
+ string(JSON <out-var> [ERROR_VARIABLE <error-var>]
+ MEMBER <json-string>
+ [<member|index> ...] <index>)
+
+Get the name of the ``<index>``:th member in ``<json-string>`` at the location
+given by the list of ``<member|index>`` arguments.
+Requires an element of object type.
+
+.. _JSONLENGTH:
+.. code-block:: cmake
+
+ string(JSON <out-var> [ERROR_VARIABLE <error-variable>]
+ LENGTH <json-string> <member|index> [<member|index> ...])
+
+Get the length of an element in ``<json-string>`` at the location
+given by the list of ``<member|index>`` arguments.
+Required an element of array or object type.
+
+.. _REMOVE:
+.. code-block:: cmake
+
+ string(JSON <out-var> [ERROR_VARIABLE <error-variable>]
+ REMOVE <json-string> <member|index> [<member|index> ...])
+
+Remove an element from ``<json-string>`` at the location
+given by the list of ``<member|index>`` arguments. The JSON string
+without the removed element will we written in ``<out-var>``.
+
+.. _SET:
+.. code-block:: cmake
+
+ string(JSON <out-var> [ERROR_VARIABLE <error-variable>]
+ SET <json-string> <member|index> [<member|index> ...] <value>)
+
+Set an element in ``<json-string>`` at the location
+given by the list of ``<member|index>`` arguments to ``<value>``.
+The contents of ``<value>`` should be valid JSON.
+
+.. _EQUAL:
+.. code-block:: cmake
+
+ string(JSON <out-var> [ERROR_VARIABLE <error-var>]
+ EQUAL <json-string1> <json-string2>)
+
+Compare the two JSON objects given by ``<json-string1>`` and ``<json-string2>``
+for equality
+
+
+If the optional ``ERROR_VARIABLE`` argument is given errors will be
+reported in ``<error-variable>``. If no error occurs the ``<error-variable>``
+will be set to ``NOTFOUND``. If ``ERROR_VARIABLE`` is not set a CMake error
+will be issued.
+When an error occurs the ``<out-var>`` will be set to
+``<member|index>-[<member|index>...]-NOTFOUND`` with the path elements up to
+the point where the error occurred.
diff --git a/Help/release/dev/string-json-support.rst b/Help/release/dev/string-json-support.rst
new file mode 100644
index 0000000000..bd7532879d
--- /dev/null
+++ b/Help/release/dev/string-json-support.rst
@@ -0,0 +1,5 @@
+string-json-support
+-------------------
+
+* The :command:`string` command gained set of new ``JSON`` sub commands to provide JSON
+ parsing capabilities.
diff --git a/Source/cmStringCommand.cxx b/Source/cmStringCommand.cxx
index 4000a7de08..b28fca954c 100644
--- a/Source/cmStringCommand.cxx
+++ b/Source/cmStringCommand.cxx
@@ -8,12 +8,21 @@
#include <cctype>
#include <cstdio>
#include <cstdlib>
+#include <initializer_list>
+#include <limits>
#include <memory>
+#include <stdexcept>
+#include <utility>
#include <cm/iterator>
+#include <cm/optional>
#include <cm/string_view>
#include <cmext/string_view>
+#include <cm3p/json/reader.h>
+#include <cm3p/json/value.h>
+#include <cm3p/json/writer.h>
+
#include "cmsys/RegularExpression.hxx"
#include "cmCryptoHash.h"
@@ -930,6 +939,296 @@ bool HandleUuidCommand(std::vector<std::string> const& args,
#endif
}
+#if !defined(CMAKE_BOOTSTRAP)
+
+// Helpers for string(JSON ...)
+struct Args : cmRange<typename std::vector<std::string>::const_iterator>
+{
+ using cmRange<typename std::vector<std::string>::const_iterator>::cmRange;
+
+ auto PopFront(cm::string_view error) -> const std::string&;
+ auto PopBack(cm::string_view error) -> const std::string&;
+};
+
+class json_error : public std::runtime_error
+{
+public:
+ json_error(std::initializer_list<cm::string_view> message,
+ cm::optional<Args> errorPath = cm::nullopt)
+ : std::runtime_error(cmCatViews(message))
+ , ErrorPath{
+ std::move(errorPath) // NOLINT(performance-move-const-arg)
+ }
+ {
+ }
+ cm::optional<Args> ErrorPath;
+};
+
+const std::string& Args::PopFront(cm::string_view error)
+{
+ if (empty()) {
+ throw json_error({ error });
+ }
+ const std::string& res = *begin();
+ advance(1);
+ return res;
+}
+
+const std::string& Args::PopBack(cm::string_view error)
+{
+ if (empty()) {
+ throw json_error({ error });
+ }
+ const std::string& res = *(end() - 1);
+ retreat(1);
+ return res;
+}
+
+cm::string_view JsonTypeToString(Json::ValueType type)
+{
+ switch (type) {
+ case Json::ValueType::nullValue:
+ return "NULL"_s;
+ case Json::ValueType::intValue:
+ case Json::ValueType::uintValue:
+ case Json::ValueType::realValue:
+ return "NUMBER"_s;
+ case Json::ValueType::stringValue:
+ return "STRING"_s;
+ case Json::ValueType::booleanValue:
+ return "BOOLEAN"_s;
+ case Json::ValueType::arrayValue:
+ return "ARRAY"_s;
+ case Json::ValueType::objectValue:
+ return "OBJECT"_s;
+ }
+ throw json_error({ "invalid JSON type found"_s });
+}
+
+int ParseIndex(
+ const std::string& str, cm::optional<Args> const& progress = cm::nullopt,
+ Json::ArrayIndex max = std::numeric_limits<Json::ArrayIndex>::max())
+{
+ unsigned long lindex;
+ if (!cmStrToULong(str, &lindex)) {
+ throw json_error({ "expected an array index, got: '"_s, str, "'"_s },
+ progress);
+ }
+ Json::ArrayIndex index = static_cast<Json::ArrayIndex>(lindex);
+ if (index >= max) {
+ cmAlphaNum sizeStr{ max };
+ throw json_error({ "expected an index less then "_s, sizeStr.View(),
+ " got '"_s, str, "'"_s },
+ progress);
+ }
+ return index;
+}
+
+Json::Value& ResolvePath(Json::Value& json, Args path)
+{
+ Json::Value* search = &json;
+
+ for (auto curr = path.begin(); curr != path.end(); ++curr) {
+ const std::string& field = *curr;
+ Args progress{ path.begin(), curr + 1 };
+
+ if (search->isArray()) {
+ auto index = ParseIndex(field, progress, search->size());
+ search = &(*search)[index];
+
+ } else if (search->isObject()) {
+ if (!search->isMember(field)) {
+ const auto progressStr = cmJoin(progress, " "_s);
+ throw json_error({ "member '"_s, progressStr, "' not found"_s },
+ progress);
+ }
+ search = &(*search)[field];
+ } else {
+ const auto progressStr = cmJoin(progress, " "_s);
+ throw json_error(
+ { "invalid path '"_s, progressStr,
+ "', need element of OBJECT or ARRAY type to lookup '"_s, field,
+ "' got "_s, JsonTypeToString(search->type()) },
+ progress);
+ }
+ }
+ return *search;
+};
+
+Json::Value ReadJson(const std::string& jsonstr)
+{
+ Json::CharReaderBuilder builder;
+ builder["collectComments"] = false;
+ auto jsonReader = std::unique_ptr<Json::CharReader>(builder.newCharReader());
+ Json::Value json;
+ std::string error;
+ if (!jsonReader->parse(jsonstr.data(), jsonstr.data() + jsonstr.size(),
+ &json, &error)) {
+ throw json_error({ "failed parsing json string: "_s, error });
+ }
+ return json;
+}
+std::string WriteJson(const Json::Value& value)
+{
+ Json::StreamWriterBuilder writer;
+ writer["indentation"] = " ";
+ writer["commentStyle"] = "None";
+ return Json::writeString(writer, value);
+}
+
+#endif
+
+bool HandleJSONCommand(std::vector<std::string> const& arguments,
+ cmExecutionStatus& status)
+{
+#if !defined(CMAKE_BOOTSTRAP)
+
+ auto& makefile = status.GetMakefile();
+ Args args{ arguments.begin() + 1, arguments.end() };
+
+ const std::string* errorVariable = nullptr;
+ const std::string* outputVariable = nullptr;
+ bool success = true;
+
+ try {
+ outputVariable = &args.PopFront("missing out-var argument"_s);
+
+ if (!args.empty() && *args.begin() == "ERROR_VARIABLE"_s) {
+ args.PopFront("");
+ errorVariable = &args.PopFront("missing error-var argument"_s);
+ makefile.AddDefinition(*errorVariable, "NOTFOUND"_s);
+ }
+
+ const auto& mode = args.PopFront("missing mode argument"_s);
+ if (mode != "GET"_s && mode != "TYPE"_s && mode != "MEMBER"_s &&
+ mode != "LENGTH"_s && mode != "REMOVE"_s && mode != "SET"_s &&
+ mode != "EQUAL"_s) {
+ throw json_error(
+ { "got an invalid mode '"_s, mode,
+ "', expected one of GET, GET_ARRAY, TYPE, MEMBER, MEMBERS,"
+ " LENGTH, REMOVE, SET, EQUAL"_s });
+ }
+
+ const auto& jsonstr = args.PopFront("missing json string argument"_s);
+ Json::Value json = ReadJson(jsonstr);
+
+ if (mode == "GET"_s) {
+ const auto& value = ResolvePath(json, args);
+ if (value.isObject() || value.isArray()) {
+ makefile.AddDefinition(*outputVariable, WriteJson(value));
+ } else if (value.isBool()) {
+ makefile.AddDefinitionBool(*outputVariable, value.asBool());
+ } else {
+ makefile.AddDefinition(*outputVariable, value.asString());
+ }
+
+ } else if (mode == "TYPE"_s) {
+ const auto& value = ResolvePath(json, args);
+ makefile.AddDefinition(*outputVariable, JsonTypeToString(value.type()));
+
+ } else if (mode == "MEMBER"_s) {
+ const auto& indexStr = args.PopBack("missing member index"_s);
+ const auto& value = ResolvePath(json, args);
+ if (!value.isObject()) {
+ throw json_error({ "MEMBER needs to be called with an element of "
+ "type OBJECT, got "_s,
+ JsonTypeToString(value.type()) },
+ args);
+ }
+ const auto index = ParseIndex(
+ indexStr, Args{ args.begin(), args.end() + 1 }, value.size());
+ const auto memIt = std::next(value.begin(), index);
+ makefile.AddDefinition(*outputVariable, memIt.name());
+
+ } else if (mode == "LENGTH"_s) {
+ const auto& value = ResolvePath(json, args);
+ if (!value.isArray() && !value.isObject()) {
+ throw json_error({ "LENGTH needs to be called with an "
+ "element of type ARRAY or OBJECT, got "_s,
+ JsonTypeToString(value.type()) },
+ args);
+ }
+
+ cmAlphaNum sizeStr{ value.size() };
+ makefile.AddDefinition(*outputVariable, sizeStr.View());
+
+ } else if (mode == "REMOVE"_s) {
+ const auto& toRemove =
+ args.PopBack("missing member or index to remove"_s);
+ auto& value = ResolvePath(json, args);
+
+ if (value.isArray()) {
+ const auto index = ParseIndex(
+ toRemove, Args{ args.begin(), args.end() + 1 }, value.size());
+ Json::Value removed;
+ value.removeIndex(index, &removed);
+
+ } else if (value.isObject()) {
+ Json::Value removed;
+ value.removeMember(toRemove, &removed);
+
+ } else {
+ throw json_error({ "REMOVE needs to be called with an "
+ "element of type ARRAY or OBJECT, got "_s,
+ JsonTypeToString(value.type()) },
+ args);
+ }
+ makefile.AddDefinition(*outputVariable, WriteJson(json));
+
+ } else if (mode == "SET"_s) {
+ const auto& newValueStr = args.PopBack("missing new value remove"_s);
+ const auto& toAdd = args.PopBack("missing member name to add"_s);
+ auto& value = ResolvePath(json, args);
+
+ Json::Value newValue = ReadJson(newValueStr);
+ if (value.isObject()) {
+ value[toAdd] = newValue;
+ } else if (value.isArray()) {
+ const auto index =
+ ParseIndex(toAdd, Args{ args.begin(), args.end() + 1 });
+ if (value.isValidIndex(index)) {
+ value[static_cast<int>(index)] = newValue;
+ } else {
+ value.append(newValue);
+ }
+ } else {
+ throw json_error({ "SET needs to be called with an "
+ "element of type OBJECT or ARRAY, got "_s,
+ JsonTypeToString(value.type()) });
+ }
+
+ makefile.AddDefinition(*outputVariable, WriteJson(json));
+
+ } else if (mode == "EQUAL"_s) {
+ const auto& jsonstr2 =
+ args.PopFront("missing second json string argument"_s);
+ Json::Value json2 = ReadJson(jsonstr2);
+ makefile.AddDefinitionBool(*outputVariable, json == json2);
+ }
+
+ } catch (const json_error& e) {
+ if (outputVariable && e.ErrorPath) {
+ const auto errorPath = cmJoin(*e.ErrorPath, "-");
+ makefile.AddDefinition(*outputVariable,
+ cmCatViews({ errorPath, "-NOTFOUND"_s }));
+ } else if (outputVariable) {
+ makefile.AddDefinition(*outputVariable, "NOTFOUND"_s);
+ }
+
+ if (errorVariable) {
+ makefile.AddDefinition(*errorVariable, e.what());
+ } else {
+ status.SetError(cmCatViews({ "sub-command JSON "_s, e.what(), "."_s }));
+ success = false;
+ }
+ }
+ return success;
+#else
+ status.SetError(cmStrCat(arguments[0], " not available during bootstrap"_s));
+ return false;
+#endif
+}
+
} // namespace
bool cmStringCommand(std::vector<std::string> const& args,
@@ -973,6 +1272,7 @@ bool cmStringCommand(std::vector<std::string> const& args,
{ "MAKE_C_IDENTIFIER"_s, HandleMakeCIdentifierCommand },
{ "GENEX_STRIP"_s, HandleGenexStripCommand },
{ "UUID"_s, HandleUuidCommand },
+ { "JSON"_s, HandleJSONCommand },
};
return subcommand(args[0], args, status);
diff --git a/Tests/RunCMake/string/JSON.cmake b/Tests/RunCMake/string/JSON.cmake
new file mode 100644
index 0000000000..ab4194d26b
--- /dev/null
+++ b/Tests/RunCMake/string/JSON.cmake
@@ -0,0 +1,342 @@
+function(assert_strequal actual expected)
+ if(NOT expected STREQUAL actual)
+ message(SEND_ERROR "Output:\n${actual}\nDid not match expected:\n${expected}\n")
+ endif()
+endfunction()
+
+function(assert_strequal_error actual expected error)
+ if(error)
+ message(SEND_ERROR "Unexpected error: ${error}")
+ endif()
+ assert_strequal("${actual}" "${expected}")
+endfunction()
+
+function(assert_json_equal error actual expected)
+ if(error)
+ message(SEND_ERROR "Unexpected error: ${error}")
+ endif()
+ string(JSON eql EQUAL "${actual}" "${expected}")
+ if(NOT eql)
+ message(SEND_ERROR "Expected equality got\n ${actual}\n expected\n${expected}")
+ endif()
+endfunction()
+
+# test EQUAL
+string(JSON result EQUAL
+[=[ {"foo":"bar"} ]=]
+[=[
+{
+"foo": "bar"
+}
+]=])
+if(NOT result)
+ message(SEND_ERROR "Expected ON got ${result}")
+endif()
+
+string(JSON result EQUAL
+[=[ {"foo":"bar"} ]=]
+[=[
+{
+"foo1": "bar"
+}
+]=])
+if(result)
+ message(SEND_ERROR "Expected OFF got ${result}")
+endif()
+
+
+
+set(json1 [=[
+{
+ "foo" : "bar",
+ "array" : [5, "val", {"some": "other"}, null],
+ "types" : {
+ "null" : null,
+ "number" : 5,
+ "string" : "foo",
+ "boolean" : false,
+ "array" : [1,2,3],
+ "object" : {}
+ },
+ "values" : {
+ "null" : null,
+ "number" : 5,
+ "string" : "foo",
+ "false" : false,
+ "true" : true
+ },
+ "special" : {
+ "foo;bar" : "value1",
+ ";" : "value2",
+ "semicolon" : ";",
+ "list" : ["one", "two;three", "four"],
+ "quote" : "\"",
+ "\"" : "quote",
+ "backslash" : "\\",
+ "\\" : "backslash",
+ "slash" : "\/",
+ "\/" : "slash",
+ "newline" : "\n",
+ "\n" : "newline",
+ "return" : "\r",
+ "\r" : "return",
+ "tab" : "\t",
+ "\t" : "tab",
+ "backspace" : "\b",
+ "\b" : "backspace",
+ "formfeed" : "\f",
+ "\f" : "formfeed"
+ }
+}
+]=])
+
+string(JSON result GET "${json1}" foo)
+assert_strequal("${result}" bar)
+string(JSON result GET "${json1}" array 0)
+assert_strequal("${result}" 5)
+string(JSON result GET "${json1}" array 1)
+assert_strequal("${result}" val)
+string(JSON result GET "${json1}" array 2 some)
+assert_strequal("${result}" other)
+
+string(JSON result GET "${json1}" values null)
+assert_strequal("${result}" "")
+string(JSON result GET "${json1}" values number)
+assert_strequal("${result}" 5)
+string(JSON result GET "${json1}" values string)
+assert_strequal("${result}" "foo")
+string(JSON result GET "${json1}" values true)
+assert_strequal("${result}" "ON")
+if(NOT result)
+ message(SEND_ERROR "Output did not match expected: TRUE actual: ${result}")
+endif()
+string(JSON result GET "${json1}" values false)
+assert_strequal("${result}" "OFF")
+if(result)
+ message(SEND_ERROR "Output did not match expected: FALSE actual: ${result}")
+endif()
+
+string(JSON result ERROR_VARIABLE error GET "${json1}" foo)
+assert_strequal_error("${result}" "bar" "${error}")
+
+string(JSON result ERROR_VARIABLE error GET "${json1}" notThere)
+assert_strequal("${result}" "notThere-NOTFOUND")
+assert_strequal("${error}" "member 'notThere' not found")
+
+string(JSON result ERROR_VARIABLE error GET "${json1}" 0)
+assert_strequal("${result}" "0-NOTFOUND")
+assert_strequal("${error}" "member '0' not found")
+
+string(JSON result ERROR_VARIABLE error GET "${json1}" array 10)
+assert_strequal("${result}" "array-10-NOTFOUND")
+assert_strequal("${error}" "expected an index less then 4 got '10'")
+
+string(JSON result ERROR_VARIABLE error GET "${json1}" array 2 some notThere)
+assert_strequal("${result}" "array-2-some-notThere-NOTFOUND")
+assert_strequal("${error}" "invalid path 'array 2 some notThere', need element of OBJECT or ARRAY type to lookup 'notThere' got STRING")
+
+# special chars
+string(JSON result ERROR_VARIABLE error GET "${json1}" special "foo;bar")
+assert_strequal_error("${result}" "value1" "${error}")
+string(JSON result ERROR_VARIABLE error GET "${json1}" special ";")
+assert_strequal_error("${result}" "value2" "${error}")
+string(JSON result ERROR_VARIABLE error GET "${json1}" special semicolon)
+assert_strequal_error("${result}" ";" "${error}")
+
+string(JSON result ERROR_VARIABLE error GET "${json1}" special list 1)
+assert_strequal_error("${result}" "two;three" "${error}")
+
+string(JSON result ERROR_VARIABLE error GET "${json1}")
+assert_json_equal("${error}" "${result}" "${json1}")
+
+string(JSON result ERROR_VARIABLE error GET "${json1}" array)
+assert_json_equal("${error}" "${result}" [=[ [5, "val", {"some": "other"}, null] ]=])
+
+string(JSON result ERROR_VARIABLE error GET "${json1}" special quote)
+assert_strequal_error("${result}" "\"" "${error}")
+string(JSON result ERROR_VARIABLE error GET "${json1}" special "\"")
+assert_strequal_error("${result}" "quote" "${error}")
+
+string(JSON result ERROR_VARIABLE error GET "${json1}" special backslash)
+assert_strequal_error("${result}" "\\" "${error}")
+string(JSON result ERROR_VARIABLE error GET "${json1}" special "\\")
+assert_strequal_error("${result}" "backslash" "${error}")
+
+string(JSON result ERROR_VARIABLE error GET "${json1}" special slash)
+assert_strequal_error("${result}" "/" "${error}")
+string(JSON result ERROR_VARIABLE error GET "${json1}" special "/")
+assert_strequal_error("${result}" "slash" "${error}")
+
+string(JSON result ERROR_VARIABLE error GET "${json1}" special newline)
+assert_strequal_error("${result}" "\n" "${error}")
+string(JSON result ERROR_VARIABLE error GET "${json1}" special "\n")
+assert_strequal_error("${result}" "newline" "${error}")
+
+string(JSON result ERROR_VARIABLE error GET "${json1}" special return)
+assert_strequal_error("${result}" "\r" "${error}")
+string(JSON result ERROR_VARIABLE error GET "${json1}" special "\r")
+assert_strequal_error("${result}" "return" "${error}")
+
+string(JSON result ERROR_VARIABLE error GET "${json1}" special tab)
+assert_strequal_error("${result}" "\t" "${error}")
+string(JSON result ERROR_VARIABLE error GET "${json1}" special "\t")
+assert_strequal_error("${result}" "tab" "${error}")
+
+file(READ ${CMAKE_CURRENT_LIST_DIR}/json/unicode.json unicode)
+string(JSON char ERROR_VARIABLE error GET "${unicode}" backspace)
+string(JSON result ERROR_VARIABLE error GET "${unicode}" "${char}")
+assert_strequal_error("${result}" "backspace" "${error}")
+
+file(READ ${CMAKE_CURRENT_LIST_DIR}/json/unicode.json unicode)
+string(JSON char ERROR_VARIABLE error GET "${unicode}" backspace)
+string(JSON result ERROR_VARIABLE error GET "${unicode}" "${char}")
+assert_strequal_error("${result}" "backspace" "${error}")
+
+string(JSON char ERROR_VARIABLE error GET "${unicode}" formfeed)
+string(JSON result ERROR_VARIABLE error GET "${unicode}" "${char}")
+assert_strequal_error("${result}" "formfeed" "${error}")
+
+string(JSON char ERROR_VARIABLE error GET "${unicode}" datalinkescape)
+string(JSON result ERROR_VARIABLE error GET "${unicode}" "${char}")
+assert_strequal_error("${result}" "datalinkescape" "${error}")
+
+# Test TYPE
+string(JSON result TYPE "${json1}" types null)
+assert_strequal("${result}" NULL)
+string(JSON result TYPE "${json1}" types number)
+assert_strequal("${result}" NUMBER)
+string(JSON result TYPE "${json1}" types string)
+assert_strequal("${result}" STRING)
+string(JSON result TYPE "${json1}" types boolean)
+assert_strequal("${result}" BOOLEAN)
+string(JSON result TYPE "${json1}" types array)
+assert_strequal("${result}" ARRAY)
+string(JSON result TYPE "${json1}" types object)
+assert_strequal("${result}" OBJECT)
+
+# Test LENGTH
+string(JSON result ERROR_VARIABLE error LENGTH "${json1}")
+assert_strequal("${result}" 5)
+if(error)
+ message(SEND_ERROR "Unexpected error: ${error}")
+endif()
+
+string(JSON result ERROR_VARIABLE error LENGTH "${json1}" array)
+assert_strequal("${result}" 4)
+if(error)
+ message(SEND_ERROR "Unexpected error: ${error}")
+endif()
+
+string(JSON result ERROR_VARIABLE error LENGTH "${json1}" foo)
+assert_strequal("${result}" "foo-NOTFOUND")
+assert_strequal("${error}" "LENGTH needs to be called with an element of type ARRAY or OBJECT, got STRING")
+
+# Test MEMBER
+string(JSON result ERROR_VARIABLE error MEMBER "${json1}" values 2)
+assert_strequal("${result}" "number")
+if(error)
+ message(SEND_ERROR "Unexpected error: ${error}")
+endif()
+
+string(JSON result ERROR_VARIABLE error MEMBER "${json1}" values 100)
+assert_strequal("${result}" "values-100-NOTFOUND")
+assert_strequal("${error}" "expected an index less then 5 got '100'")
+
+# Test length loops
+string(JSON arrayLength ERROR_VARIABLE error LENGTH "${json1}" types array)
+if(error)
+ message(SEND_ERROR "Unexpected error: ${error}")
+endif()
+set(values "")
+math(EXPR arrayLength "${arrayLength}-1")
+foreach(index RANGE ${arrayLength})
+ string(JSON value ERROR_VARIABLE error GET "${json1}" types array ${index})
+ if(error)
+ message(SEND_ERROR "Unexpected error: ${error}")
+ endif()
+ list(APPEND values "${value}")
+endforeach()
+assert_strequal("${values}" "1;2;3")
+
+string(JSON valuesLength ERROR_VARIABLE error LENGTH "${json1}" values)
+if(error)
+ message(SEND_ERROR "Unexpected error: ${error}")
+endif()
+set(values "")
+set(members "")
+math(EXPR valuesLength "${valuesLength}-1")
+foreach(index RANGE ${valuesLength})
+ string(JSON member ERROR_VARIABLE error MEMBER "${json1}" values ${index})
+ if(error)
+ message(SEND_ERROR "Unexpected error: ${error}")
+ endif()
+ string(JSON value ERROR_VARIABLE error GET "${json1}" values ${member})
+ if(error)
+ message(SEND_ERROR "Unexpected error: ${error}")
+ endif()
+
+ list(APPEND members "${member}")
+ list(APPEND values "${value}")
+endforeach()
+assert_strequal("${members}" "false;null;number;string;true")
+assert_strequal("${values}" "OFF;;5;foo;ON")
+
+# Test REMOVE
+set(json2 [=[{
+ "foo" : "bar",
+ "array" : [5, "val", {"some": "other"}, null]
+}]=])
+string(JSON result ERROR_VARIABLE error REMOVE ${json2} foo)
+assert_json_equal("${error}" "${result}"
+[=[{
+ "array" : [5, "val", {"some": "other"}, null]
+}]=])
+
+string(JSON result ERROR_VARIABLE error REMOVE ${json2} array 1)
+assert_json_equal("${error}" "${result}"
+[=[{
+ "foo" : "bar",
+ "array" : [5, {"some": "other"}, null]
+}]=])
+
+string(JSON result ERROR_VARIABLE error REMOVE ${json2} array 100)
+assert_strequal("${result}" "array-100-NOTFOUND")
+assert_strequal("${error}" "expected an index less then 4 got '100'")
+
+# Test SET
+string(JSON result ERROR_VARIABLE error SET ${json2} new 5)
+assert_json_equal("${error}" "${result}"
+[=[{
+ "foo" : "bar",
+ "array" : [5, "val", {"some": "other"}, null],
+ "new" : 5
+}]=])
+
+string(JSON result ERROR_VARIABLE error SET ${json2} new [=[ {"obj" : false} ]=])
+assert_json_equal("${error}" "${result}"
+[=[{
+ "foo" : "bar",
+ "array" : [5, "val", {"some": "other"}, null],
+ "new" : {"obj" : false}
+}]=])
+
+string(JSON result ERROR_VARIABLE error SET ${json2} array 0 6)
+assert_json_equal("${error}" "${result}"
+[=[{
+ "foo" : "bar",
+ "array" : [6, "val", {"some": "other"}, null]
+}]=])
+
+string(JSON result ERROR_VARIABLE error SET ${json2} array 5 [["append"]])
+assert_json_equal("${error}" "${result}"
+[=[{
+ "foo" : "bar",
+ "array" : [5, "val", {"some": "other"}, null, "append"]
+}]=])
+
+string(JSON result ERROR_VARIABLE error SET ${json2} array 100 [["append"]])
+assert_json_equal("${error}" "${result}"
+[=[{
+ "foo" : "bar",
+ "array" : [5, "val", {"some": "other"}, null, "append"]
+}]=])
diff --git a/Tests/RunCMake/string/JSONNoArgs-result.txt b/Tests/RunCMake/string/JSONNoArgs-result.txt
new file mode 100644
index 0000000000..d00491fd7e
--- /dev/null
+++ b/Tests/RunCMake/string/JSONNoArgs-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/string/JSONNoArgs-stderr.txt b/Tests/RunCMake/string/JSONNoArgs-stderr.txt
new file mode 100644
index 0000000000..426735ac47
--- /dev/null
+++ b/Tests/RunCMake/string/JSONNoArgs-stderr.txt
@@ -0,0 +1,4 @@
+CMake Error at JSONNoArgs.cmake:1 \(string\):
+ string sub-command JSON missing out-var argument.
+Call Stack \(most recent call first\):
+ CMakeLists\.txt:[0-9]+ \(include\)
diff --git a/Tests/RunCMake/string/JSONNoArgs.cmake b/Tests/RunCMake/string/JSONNoArgs.cmake
new file mode 100644
index 0000000000..4463fe4a27
--- /dev/null
+++ b/Tests/RunCMake/string/JSONNoArgs.cmake
@@ -0,0 +1 @@
+string(JSON)
diff --git a/Tests/RunCMake/string/JSONNoJson-result.txt b/Tests/RunCMake/string/JSONNoJson-result.txt
new file mode 100644
index 0000000000..d00491fd7e
--- /dev/null
+++ b/Tests/RunCMake/string/JSONNoJson-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/string/JSONNoJson-stderr.txt b/Tests/RunCMake/string/JSONNoJson-stderr.txt
new file mode 100644
index 0000000000..26804dfeb3
--- /dev/null
+++ b/Tests/RunCMake/string/JSONNoJson-stderr.txt
@@ -0,0 +1,4 @@
+CMake Error at JSONNoJson.cmake:1 \(string\):
+ string sub-command JSON missing json string argument.
+Call Stack \(most recent call first\):
+ CMakeLists\.txt:[0-9]+ \(include\)
diff --git a/Tests/RunCMake/string/JSONNoJson.cmake b/Tests/RunCMake/string/JSONNoJson.cmake
new file mode 100644
index 0000000000..4f819f16e2
--- /dev/null
+++ b/Tests/RunCMake/string/JSONNoJson.cmake
@@ -0,0 +1 @@
+string(JSON var GET)
diff --git a/Tests/RunCMake/string/JSONOneArg-result.txt b/Tests/RunCMake/string/JSONOneArg-result.txt
new file mode 100644
index 0000000000..d00491fd7e
--- /dev/null
+++ b/Tests/RunCMake/string/JSONOneArg-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/string/JSONOneArg-stderr.txt b/Tests/RunCMake/string/JSONOneArg-stderr.txt
new file mode 100644
index 0000000000..282139cf1c
--- /dev/null
+++ b/Tests/RunCMake/string/JSONOneArg-stderr.txt
@@ -0,0 +1,4 @@
+CMake Error at JSONOneArg.cmake:1 \(string\):
+ string sub-command JSON missing mode argument.
+Call Stack \(most recent call first\):
+ CMakeLists\.txt:[0-9]+ \(include\)
diff --git a/Tests/RunCMake/string/JSONOneArg.cmake b/Tests/RunCMake/string/JSONOneArg.cmake
new file mode 100644
index 0000000000..143f9ca1a4
--- /dev/null
+++ b/Tests/RunCMake/string/JSONOneArg.cmake
@@ -0,0 +1 @@
+string(JSON var)
diff --git a/Tests/RunCMake/string/JSONWrongMode-result.txt b/Tests/RunCMake/string/JSONWrongMode-result.txt
new file mode 100644
index 0000000000..d00491fd7e
--- /dev/null
+++ b/Tests/RunCMake/string/JSONWrongMode-result.txt
@@ -0,0 +1 @@
+1
diff --git a/Tests/RunCMake/string/JSONWrongMode-stderr.txt b/Tests/RunCMake/string/JSONWrongMode-stderr.txt
new file mode 100644
index 0000000000..c70991b272
--- /dev/null
+++ b/Tests/RunCMake/string/JSONWrongMode-stderr.txt
@@ -0,0 +1,5 @@
+CMake Error at JSONWrongMode.cmake:1 \(string\):
+ string sub-command JSON got an invalid mode 'FOO', expected one of GET,
+ GET_ARRAY, TYPE, MEMBER, MEMBERS, LENGTH, REMOVE, SET, EQUAL.
+Call Stack \(most recent call first\):
+ CMakeLists\.txt:[0-9]+ \(include\)
diff --git a/Tests/RunCMake/string/JSONWrongMode.cmake b/Tests/RunCMake/string/JSONWrongMode.cmake
new file mode 100644
index 0000000000..7301f7a880
--- /dev/null
+++ b/Tests/RunCMake/string/JSONWrongMode.cmake
@@ -0,0 +1 @@
+string(JSON var FOO)
diff --git a/Tests/RunCMake/string/RunCMakeTest.cmake b/Tests/RunCMake/string/RunCMakeTest.cmake
index bb7cb174ef..ff0bb51a3b 100644
--- a/Tests/RunCMake/string/RunCMakeTest.cmake
+++ b/Tests/RunCMake/string/RunCMakeTest.cmake
@@ -1,5 +1,12 @@
include(RunCMake)
+run_cmake(JSON)
+
+run_cmake(JSONNoJson)
+run_cmake(JSONWrongMode)
+run_cmake(JSONOneArg)
+run_cmake(JSONNoArgs)
+
run_cmake(Append)
run_cmake(AppendNoArgs)
diff --git a/Tests/RunCMake/string/json/unicode.json b/Tests/RunCMake/string/json/unicode.json
new file mode 100644
index 0000000000..86fd232c4e
--- /dev/null
+++ b/Tests/RunCMake/string/json/unicode.json
@@ -0,0 +1,8 @@
+{
+ "backspace" : "\b",
+ "\b" : "backspace",
+ "formfeed" : "\f",
+ "\f" : "formfeed" ,
+ "datalinkescape" : "\u0010",
+ "\u0010" : "datalinkescape"
+}